HISE Logo Forum
    • Categories
    • Register
    • Login

    Random, Cycle, Synthetic, Hybrid Round Robin

    Scheduled Pinned Locked Moved Presets / Scripts / Ideas
    13 Posts 2 Posters 3.8k Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • d.healeyD
      d.healey
      last edited by d.healey

      Thanks for the tips, especially the efficiency related ones, I need to go through some of my other scripts and incorporate these ideas too :) I didn't know that about const or that for in could be used in that context, I shall definitely make good use of this.

      EDIT: I've implemented those suggestions now, here is the updated version:

      
      <Processor Type="ScriptProcessor" ID="Multi Round Robin" Bypassed="0" Script="/**&#10; * Multi-Round Robin script v2.0.js&#10; * Author: David Healey&#10; * Date: 07/01/2017&#10; * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html&#10; */&#10;&#10;//Includes &#10;&#10;Content.setHeight(100);&#10;&#10;//Init&#10;reg excludeSamplers = [&quot;Release&quot;]; //Samples with these words in their ID won't be affected&#10;const var samplerNames = Synth.getIdList(&quot;Sampler&quot;); //Get the ids of all child samplers&#10;reg samplers = [];&#10;&#10;const var group = Engine.createMidiList(); //For group based RR&#10;reg groupValue; //Value taken from the group MIDI list&#10;const var offset = Engine.createMidiList(); //For synthetic RR&#10;reg offsetValue; //Value taken from the offset MIDI list&#10;const var lastGroup = Engine.createMidiList();&#10;const var lastOffset = Engine.createMidiList();&#10;&#10;const var groupCounter = Engine.createMidiList();&#10;const var offsetCounter = Engine.createMidiList();&#10;groupCounter.fill(0);&#10;offsetCounter.fill(0);&#10;&#10;reg groupHistory = [];&#10;reg offsetHistory = [];&#10;&#10;reg numGroups; //Script assumes all samplers have the same number of groups&#10;reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected&#10;&#10;const var timer = Engine.createMidiList();&#10;&#10;namespace RRModes&#10;{&#10;&#9;const var CYCLE = 1;&#10;&#9;const var TRUE_RANDOM = 2;&#10;&#9;const var RANDOM_NO_REPEAT = 3;&#10;&#9;const var RANDOM_FULL_CYCLE = 4;&#10;};&#10;&#10;//Get child samplers&#10;for (i = 0; i &lt; samplerNames.length; i++)&#10;{&#10;&#9;for (j = 0; j &lt; excludeSamplers.length; j++)&#10;&#9;{&#10;&#9;&#9;if (samplerNames[i].indexOf(excludeSamplers[j]) !== -1) continue; //Skip exluded IDs&#10;&#9;}&#10;&#10;&#9;samplers.push(Synth.getSampler(samplerNames[i])); //Add sampler to array&#10;}&#10;&#10;for (sampler in samplers)&#10;{&#10;&#9;sampler.enableRoundRobin(false); //Disable default RR behaviour&#10;&#9;sampler.refreshRRMap();&#10;}&#10;&#10;//Create offset history MIDI lists - same is done for groupHistory in on control callback&#10;for (i = 0; i &lt; numOffsets; i++) //3 possible offset states for synthetic RR&#10;{&#10;&#9;offsetHistory[i] = Engine.createMidiList();&#10;&#9;offsetHistory[i].fill(-1);&#10;}&#10;&#10;//GUI&#10;//RR Type and Mode Combo Boxes&#10;const var cmbType = Content.addComboBox(&quot;Type&quot;, 0, 10);&#10;cmbType.set(&quot;items&quot;, [&quot;Off&quot;, &quot;Real&quot;, &quot;Synthetic&quot;, &quot;Hybrid&quot;].join(&quot;\n&quot;));&#10;&#10;const var cmbMode = Content.addComboBox(&quot;Mode&quot;, 150, 10);&#10;cmbMode.set(&quot;items&quot;, [&quot;Cycle&quot;, &quot;Random&quot;, &quot;Random No Repeat&quot;, &quot;Random Full Cycle&quot;].join(&quot;\n&quot;));&#10;&#10;//Playable Range controls&#10;const var knbLowNote = Content.addKnob(&quot;Low Note&quot;, 300, 0);&#10;const var knbHighNote = Content.addKnob(&quot;High Note&quot;, 450, 0);&#10;knbLowNote.setRange(0, 127, 1);&#10;knbHighNote.setRange(0, 127, 1);&#10;&#10;//Reset timeout knob&#10;const var knbReset = Content.addKnob(&quot;Reset Time&quot;, 600, 0);&#10;knbReset.setRange(0, 60, 1);&#10;&#10;//Microtuning knob&#10;const var knbMicroTuning = Content.addKnob(&quot;Microtuning&quot;, 0, 50);&#10;knbMicroTuning.setRange(0, 100, 0.1);&#10;&#10;//Functions&#10;&#10;//Callbacks&#10;function onNoteOn()&#10;{&#10;&#9;local i; &#10;&#10;&#9;if (cmbType.getValue() &gt; 1 &amp;&amp; Message.getNoteNumber() &gt;= knbLowNote.getValue() &amp;&amp; Message.getNoteNumber() &lt;= knbHighNote.getValue()) //RR is not off&#10;&#9;{&#10;&#9;&#9;//Group based RR&#10;&#9;&#9;if (cmbType.getValue() == 2 || cmbType.getValue() == 4) //Real or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;if (numGroups != 1)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), (group.getValue(Message.getNoteNumber()) + 1) % numGroups);&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.TRUE_RANDOM:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.RANDOM_NO_REPEAT:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group.getValue(Message.getNoteNumber()) == lastGroup.getValue(Message.getNoteNumber()))&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.RANDOM_FULL_CYCLE:&#10;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;&#9;if (groupCounter.getValue(Message.getNoteNumber()) &gt;= numGroups)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;groupCounter.setValue(Message.getNoteNumber(), 0); //Reset groupCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;for (i = 0; i &lt; numGroups; i++)&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;groupHistory[i].setValue(Message.getNoteNumber(), -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group.getValue(Message.getNoteNumber()) == lastGroup.getValue(Message.getNoteNumber()) || groupHistory[group.getValue(Message.getNoteNumber())].getValue(Message.getNoteNumber()) != -1)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numGroups));&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;else &#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), 0);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset group if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(Message.getNoteNumber()) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group.setValue(Message.getNoteNumber(), 0);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;groupValue = group.getValue(Message.getNoteNumber()); //Current group value for this note&#10;&#10;&#9;&#9;&#9;//Set active group for each samper&#10;&#9;&#9;&#9;for (sampler in samplers)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;sampler.setActiveGroup(1+groupValue);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;groupCounter.setValue(Message.getNoteNumber(), groupCounter.getValue(Message.getNoteNumber()) + 1); //Increment counter&#10;&#9;&#9;&#9;groupHistory[groupValue].setValue(Message.getNoteNumber(), 1); //Mark RR as used&#10;&#9;&#9;&#9;lastGroup.setValue(Message.getNoteNumber(), groupValue);&#10;&#9;&#9;}&#10;&#9;&#9;&#10;&#9;&#9;//Synthetic sample borrowed RR&#10;&#9;&#9;if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;case RRModes.CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), (offset.getValue(Message.getNoteNumber()) + 1) % numOffsets);&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.TRUE_RANDOM:&#10;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;while ((offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() &gt; knbHighNote.getValue() &amp;&amp; (offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() &lt; knbLowNote.getValue())&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.RANDOM_NO_REPEAT:&#10;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;while (offset.getValue(Message.getNoteNumber()) == lastOffset.getValue(Message.getNoteNumber()) || ((offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() &gt; knbHighNote.getValue() &amp;&amp; (offset.getValue(Message.getNoteNumber())-1) + Message.getNoteNumber() &lt; knbLowNote.getValue()))&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.RANDOM_FULL_CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;if (offsetCounter.getValue(Message.getNoteNumber()) &gt;= numOffsets)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offsetCounter.setValue(Message.getNoteNumber(), 0); //Reset offsetCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;for (i = 0; i &lt; numOffsets; i++)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;offsetHistory[i].setValue(Message.getNoteNumber(), -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#10;&#9;&#9;&#9;&#9;&#9;while (offset.getValue(Message.getNoteNumber()) == lastOffset.getValue(Message.getNoteNumber()) || offsetHistory[offset.getValue(Message.getNoteNumber())].getValue(Message.getNoteNumber()) != -1)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), Math.floor(Math.random() * numOffsets));&#9;&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset offset if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(Message.getNoteNumber()) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), 0);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Make sure offset + note is not outside playable range&#10;&#9;&#9;&#9;if (offset.getValue(Message.getNoteNumber())-1 + Message.getNoteNumber() &lt; knbLowNote.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), 1);&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;else if (offset.getValue(Message.getNoteNumber())-1 + Message.getNoteNumber() &gt; knbHighNote.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset.setValue(Message.getNoteNumber(), 0);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;offsetValue = offset.getValue(Message.getNoteNumber());&#10;&#10;&#9;&#9;&#9;//Apply offset and coarse tuning to message&#10;&#9;&#9;&#9;Message.setTransposeAmount(offsetValue-1); //Use -1 to make offset range -1 to +1&#10;&#9;&#9;&#9;Message.setCoarseDetune(-(offsetValue-1)); //Use coarse detune so setting can be picked up by later scripts&#10;&#10;&#9;&#9;&#9;offsetCounter.setValue(Message.getNoteNumber(), offsetCounter.getValue(Message.getNoteNumber()) + 1); //Increment counter&#10;&#9;&#9;&#9;offsetHistory[offsetValue].setValue(Message.getNoteNumber(), 1); //Mark RR as used&#10;&#9;&#9;&#9;lastOffset.setValue(Message.getNoteNumber(), offsetValue);&#10;&#9;&#9;}&#10;&#10;&#9;&#9;//Apply random microtuning, if any&#10;&#9;&#9;if (knbMicroTuning.getValue() != 0)&#10;&#9;&#9;{&#10;&#9;&#9;&#9;Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue());&#10;&#9;&#9;}&#10;&#10;&#9;&#9;timer.setValue(Message.getNoteNumber(), Engine.getUptime());&#10;&#9;}&#10;}&#10;&#10;function onNoteOff()&#10;{&#10;&#9;&#10;}&#10;function onController()&#10;{&#10;&#9;&#10;}&#10;function onTimer()&#10;{&#10;&#9;&#10;}&#10;function onControl(number, value)&#10;{&#10;&#9;switch (number)&#10;&#9;{&#10;&#9;&#9;case knbHighNote:&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;//Get number of groups - assumes all samplers have the same number of groups and samples are mapped over velocity 64&#10;&#9;&#9;&#9;numGroups = samplers.length != 0 ? samplers[0].getRRGroupsForMessage(value, 64) : 0;&#10;&#10;&#9;&#9;&#9;groupHistory = [];&#10;&#9;&#9;&#9;//Create group history MIDI lists&#10;&#9;&#9;&#9;for (i = 0; i &lt; numGroups; i++) //One MIDI list per group&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;groupHistory[i] = Engine.createMidiList();&#10;&#9;&#9;&#9;&#9;groupHistory[i].fill(-1);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;break;&#10;&#9;}&#10;}">
        <EditorStates BodyShown="1" Visible="1" Solo="0" contentShown="1" onInitOpen="0"
                      onNoteOnOpen="0" onNoteOffOpen="0" onControllerOpen="0" onTimerOpen="0"
                      onControlOpen="0" Folded="0"/>
        <ChildProcessors/>
        <Content>
          <Control type="ScriptComboBox" id="Type" value="1"/>
          <Control type="ScriptComboBox" id="Mode" value="3"/>
          <Control type="ScriptSlider" id="Low Note" value="60"/>
          <Control type="ScriptSlider" id="High Note" value="96"/>
          <Control type="ScriptSlider" id="Reset Time" value="5"/>
          <Control type="ScriptSlider" id="Microtuning" value="0"/>
        </Content>
      </Processor>```

      Libre Wave - Freedom respecting instruments and effects
      My Patreon - HISE tutorials
      YouTube Channel - Public HISE tutorials

      1 Reply Last reply Reply Quote 0
      • d.healeyD
        d.healey
        last edited by

        Small Update: Couple of bug fixes and improved readability.

        <?xml version="1.0" encoding="UTF-8"?>
        
        <Processor Type="ScriptProcessor" ID="Multi Round Robin" Bypassed="0" Script="/**&#10; * Multi-Round Robin script v2.2&#10; * Author: David Healey&#10; * Date: 07/01/2017&#10; * Modified: 09/01/2017&#10; * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html&#10; */&#10;&#10;//Includes &#10;&#10;Content.setHeight(100);&#10;&#10;//Init&#10;reg excludeSamplers = [&quot;Release&quot;]; //Samples with these words in their ID won't be affected&#10;const var samplerNames = Synth.getIdList(&quot;Sampler&quot;); //Get the ids of all child samplers&#10;reg samplers = [];&#10;&#10;reg group; //Group RR counter&#10;reg offset; //Synthetic RR counter&#10;&#10;const var lastGroup = Engine.createMidiList();&#10;const var lastOffset = Engine.createMidiList();&#10;&#10;const var groupCounter = Engine.createMidiList();&#10;const var offsetCounter = Engine.createMidiList();&#10;groupCounter.fill(0);&#10;offsetCounter.fill(0);&#10;&#10;reg groupHistory = [];&#10;reg offsetHistory = [];&#10;&#10;reg numGroups; //Script assumes all samplers have the same number of groups&#10;reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected&#10;&#10;reg lowNote;&#10;reg highNote;&#10;reg noteNumber;&#10;&#10;const var timer = Engine.createMidiList();&#10;&#10;namespace RRModes&#10;{&#10;&#9;const var CYCLE = 1;&#10;&#9;const var TRUE_RANDOM = 2;&#10;&#9;const var RANDOM_NO_REPEAT = 3;&#10;&#9;const var RANDOM_FULL_CYCLE = 4;&#10;};&#10;&#10;//Get child samplers&#10;for (i = 0; i &lt; samplerNames.length; i++)&#10;{&#10;&#9;for (j = 0; j &lt; excludeSamplers.length; j++)&#10;&#9;{&#10;&#9;&#9;if (samplerNames[i].indexOf(excludeSamplers[j]) !== -1) continue; //Skip exluded IDs&#10;&#9;}&#10;&#10;&#9;samplers.push(Synth.getSampler(samplerNames[i])); //Add sampler to array&#10;}&#10;&#10;for (sampler in samplers)&#10;{&#10;&#9;sampler.enableRoundRobin(false); //Disable default RR behaviour&#10;&#9;sampler.refreshRRMap();&#10;}&#10;&#10;//Create offset history MIDI lists - same is done for groupHistory in on control callback&#10;for (i = 0; i &lt; numOffsets; i++) //3 possible offset states for synthetic RR&#10;{&#10;&#9;offsetHistory[i] = Engine.createMidiList();&#10;&#9;offsetHistory[i].fill(-1);&#10;}&#10;&#10;//GUI&#10;//RR Type and Mode Combo Boxes&#10;const var cmbType = Content.addComboBox(&quot;Type&quot;, 0, 10);&#10;cmbType.set(&quot;items&quot;, [&quot;Off&quot;, &quot;Real&quot;, &quot;Synthetic&quot;, &quot;Hybrid&quot;].join(&quot;\n&quot;));&#10;&#10;const var cmbMode = Content.addComboBox(&quot;Mode&quot;, 150, 10);&#10;cmbMode.set(&quot;items&quot;, [&quot;Cycle&quot;, &quot;Random&quot;, &quot;Random No Repeat&quot;, &quot;Random Full Cycle&quot;].join(&quot;\n&quot;));&#10;&#10;//Playable Range controls&#10;const var knbLowNote = Content.addKnob(&quot;Low Note&quot;, 300, 0);&#10;const var knbHighNote = Content.addKnob(&quot;High Note&quot;, 450, 0);&#10;knbLowNote.setRange(0, 127, 1);&#10;knbHighNote.setRange(0, 127, 1);&#10;&#10;//Reset timeout knob&#10;const var knbReset = Content.addKnob(&quot;Reset Time&quot;, 600, 0);&#10;knbReset.setRange(0, 60, 1);&#10;&#10;//Microtuning knob&#10;const var knbMicroTuning = Content.addKnob(&quot;Microtuning&quot;, 0, 50);&#10;knbMicroTuning.setRange(0, 100, 0.1);&#10;&#10;//Functions&#10;&#10;//Callbacks&#10;function onNoteOn()&#10;{&#10;&#9;local i;&#10;&#9;noteNumber = Message.getNoteNumber();&#10;&#10;&#9;if (cmbType.getValue() &gt; 1 &amp;&amp; noteNumber &gt;= lowNote &amp;&amp; noteNumber &lt;= highNote) //RR is not off&#10;&#9;{&#10;&#9;&#9;//Group based RR&#10;&#9;&#9;if (cmbType.getValue() == 2 || cmbType.getValue() == 4) //Real or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;if (numGroups != 1)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = (lastGroup.getValue(Message.getNoteNumber()) + 1) % numGroups;&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.TRUE_RANDOM:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = Math.floor(Math.random() * numGroups);&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.RANDOM_NO_REPEAT:&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = -1;&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group == -1 || group == lastGroup.getValue(noteNumber))&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group = Math.floor(Math.random() * numGroups);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.RANDOM_FULL_CYCLE:&#10;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;&#9;if (groupCounter.getValue(noteNumber) &gt;= numGroups)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;groupCounter.setValue(noteNumber, 0); //Reset groupCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;for (gHistory in groupHistory)&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;gHistory.setValue(noteNumber, -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = -1;&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group == -1 || group == lastGroup.getValue(noteNumber) || groupHistory[group].getValue(noteNumber) != -1)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group = Math.floor(Math.random() * numGroups);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;else &#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group = 0;&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset group if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(noteNumber) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group = 0;&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;//Set active group for each samper&#10;&#9;&#9;&#9;for (sampler in samplers)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;sampler.setActiveGroup(1+group);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;groupCounter.setValue(noteNumber, groupCounter.getValue(noteNumber) + 1); //Increment counter&#10;&#9;&#9;&#9;groupHistory[group].setValue(noteNumber, 1); //Mark RR as used&#10;&#9;&#9;&#9;lastGroup.setValue(noteNumber, group);&#10;&#9;&#9;}&#10;&#9;&#9;&#10;&#9;&#9;//Synthetic sample borrowed RR&#10;&#9;&#9;if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;offset = -1;&#10;&#10;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;case RRModes.CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;offset = (lastOffset.getValue(noteNumber) + 1) % numOffsets;&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.TRUE_RANDOM:&#10;&#9;&#9;&#9;&#9;&#9;offset = Math.floor(Math.random() * numOffsets);&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.RANDOM_NO_REPEAT:&#10;&#10;&#9;&#9;&#9;&#9;&#9;while (offset == -1 || offset == lastOffset.getValue(noteNumber))&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset = Math.floor(Math.random() * numOffsets);&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.RANDOM_FULL_CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;if (offsetCounter.getValue(noteNumber) &gt;= numOffsets)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offsetCounter.setValue(noteNumber, 0); //Reset offsetCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;for (oHistory in offsetHistory)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;oHistory.setValue(noteNumber, -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;while (offset == -1 || offset == lastOffset.getValue(noteNumber) || offsetHistory[offset].getValue(noteNumber) != -1)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset = Math.floor(Math.random() * numOffsets);&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset offset if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(noteNumber) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset = 0;&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;if (noteNumber + offset - 1 &lt; lowNote) offset = 1;&#10;&#9;&#9;&#9;if (noteNumber + offset - 1 &gt; highNote) offset = 0;&#10;&#10;&#9;&#9;&#9;//Apply offset and coarse tuning to message&#10;&#9;&#9;&#9;Message.setTransposeAmount(offset-1); //Use -1 to make offset range -1 to +1&#10;&#9;&#9;&#9;Message.setCoarseDetune(-(offset-1)); //Use coarse detune so setting can be picked up by later scripts&#10;&#10;&#9;&#9;&#9;offsetCounter.setValue(noteNumber, offsetCounter.getValue(noteNumber) + 1); //Increment counter&#10;&#9;&#9;&#9;offsetHistory[offset].setValue(noteNumber, 1); //Mark RR as used&#10;&#9;&#9;&#9;lastOffset.setValue(noteNumber, offset);&#10;&#9;&#9;}&#10;&#10;&#9;&#9;//Apply random microtuning, if any&#10;&#9;&#9;if (knbMicroTuning.getValue() != 0)&#10;&#9;&#9;{&#10;&#9;&#9;&#9;Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue());&#10;&#9;&#9;}&#10;&#10;&#9;&#9;timer.setValue(noteNumber, Engine.getUptime());&#10;&#9;}&#10;}&#10;&#10;function onNoteOff()&#10;{&#10;&#9;&#10;}&#10;function onController()&#10;{&#10;&#9;&#10;}&#10;function onTimer()&#10;{&#10;&#9;&#10;}&#10;function onControl(number, value)&#10;{&#10;&#9;switch (number)&#10;&#9;{&#10;&#9;&#9;case knbLowNote:&#10;&#9;&#9;&#9;lowNote = value;&#10;&#9;&#9;break;&#10;&#10;&#9;&#9;case knbHighNote:&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;highNote = value;&#10;&#10;&#9;&#9;&#9;//Get number of groups - assumes all samplers have the same number of groups and samples are mapped over velocity 64&#10;&#9;&#9;&#9;numGroups = samplers.length != 0 ? samplers[0].getRRGroupsForMessage(value, 64) : 0;&#10;&#10;&#9;&#9;&#9;groupHistory = [];&#10;&#9;&#9;&#9;//Create group history MIDI lists&#10;&#9;&#9;&#9;for (i = 0; i &lt; numGroups; i++) //One MIDI list per group&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;groupHistory[i] = Engine.createMidiList();&#10;&#9;&#9;&#9;&#9;groupHistory[i].fill(-1);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;break;&#10;&#9;}&#10;}">
          <EditorStates BodyShown="1" Visible="1" Solo="0" contentShown="1" onInitOpen="0"
                        onNoteOnOpen="1" onNoteOffOpen="0" onControllerOpen="0" onTimerOpen="0"
                        onControlOpen="0" Folded="0"/>
          <ChildProcessors/>
          <Content>
            <Control type="ScriptComboBox" id="Type" value="2"/>
            <Control type="ScriptComboBox" id="Mode" value="1"/>
            <Control type="ScriptSlider" id="Low Note" value="60"/>
            <Control type="ScriptSlider" id="High Note" value="96"/>
            <Control type="ScriptSlider" id="Reset Time" value="5"/>
            <Control type="ScriptSlider" id="Microtuning" value="0"/>
          </Content>
        </Processor>
        

        Libre Wave - Freedom respecting instruments and effects
        My Patreon - HISE tutorials
        YouTube Channel - Public HISE tutorials

        1 Reply Last reply Reply Quote 0
        • d.healeyD
          d.healey
          last edited by

          Version 2.3: Some code improvements. The exclude sampler part now uses a regex string instead of indexOf() and an array of strings.

          <?xml version="1.0" encoding="UTF-8"?>
          
          <Processor Type="ScriptProcessor" ID="Multi Round Robin" Bypassed="0" Script="/**&#10; * Multi-Round Robin script v2.3&#10; * Author: David Healey&#10; * Date: 07/01/2017&#10; * Modified: 13/01/2017&#10; * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html&#10; */&#10;&#10;//Includes &#10;&#10;Content.setHeight(100);&#10;&#10;//Init&#10;const var excludedIds = &quot;(elease)&quot;; //Regex string of words in sampler IDs to exclude RR from&#10;const var samplerNames = Synth.getIdList(&quot;Sampler&quot;); //Get the ids of all child samplers&#10;const var samplers = [];&#10;&#10;reg group; //Group RR counter&#10;reg offset; //Synthetic RR counter&#10;&#10;const var lastGroup = Engine.createMidiList();&#10;const var lastOffset = Engine.createMidiList();&#10;&#10;const var groupCounter = Engine.createMidiList();&#10;const var offsetCounter = Engine.createMidiList();&#10;groupCounter.fill(0);&#10;offsetCounter.fill(0);&#10;&#10;const var groupHistory = [];&#10;const var offsetHistory = [];&#10;&#10;reg numGroups; //Script assumes all samplers have the same number of groups&#10;reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected&#10;&#10;reg lowNote;&#10;reg highNote;&#10;reg noteNumber;&#10;&#10;const var timer = Engine.createMidiList();&#10;&#10;namespace RRModes&#10;{&#10;&#9;const var CYCLE = 1;&#10;&#9;const var TRUE_RANDOM = 2;&#10;&#9;const var RANDOM_NO_REPEAT = 3;&#10;&#9;const var RANDOM_FULL_CYCLE = 4;&#10;};&#10;&#10;//Get child samplers&#10;for (samplerName in samplerNames)&#10;{&#10;&#9;if (Engine.matchesRegex(samplerName, excludedIds) == true) continue; //Skip excluded IDs&#10;&#10;&#9;samplers.push(Synth.getSampler(samplerName)); //Add sampler to array&#10;&#9;samplers[samplers.length-1].enableRoundRobin(false); //Disable default RR behaviour&#10;&#9;samplers[samplers.length-1].refreshRRMap();&#10;}&#10;&#10;//Create offset history MIDI lists - same is done for groupHistory in on control callback&#10;for (i = 0; i &lt; numOffsets; i++) //3 possible offset states for synthetic RR&#10;{&#10;&#9;offsetHistory[i] = Engine.createMidiList();&#10;&#9;offsetHistory[i].fill(-1);&#10;}&#10;&#10;//GUI&#10;//RR Type and Mode Combo Boxes&#10;const var cmbType = Content.addComboBox(&quot;Type&quot;, 0, 10);&#10;cmbType.set(&quot;items&quot;, [&quot;Off&quot;, &quot;Real&quot;, &quot;Synthetic&quot;, &quot;Hybrid&quot;].join(&quot;\n&quot;));&#10;&#10;const var cmbMode = Content.addComboBox(&quot;Mode&quot;, 150, 10);&#10;cmbMode.set(&quot;items&quot;, [&quot;Cycle&quot;, &quot;Random&quot;, &quot;Random No Repeat&quot;, &quot;Random Full Cycle&quot;].join(&quot;\n&quot;));&#10;&#10;//Playable Range controls&#10;const var knbLowNote = Content.addKnob(&quot;Low Note&quot;, 300, 0);&#10;const var knbHighNote = Content.addKnob(&quot;High Note&quot;, 450, 0);&#10;knbLowNote.setRange(0, 127, 1);&#10;knbHighNote.setRange(0, 127, 1);&#10;&#10;//Reset timeout knob&#10;const var knbReset = Content.addKnob(&quot;Reset Time&quot;, 600, 0);&#10;knbReset.setRange(0, 60, 1);&#10;&#10;//Microtuning knob&#10;const var knbMicroTuning = Content.addKnob(&quot;Microtuning&quot;, 0, 50);&#10;knbMicroTuning.setRange(0, 100, 0.1);&#10;&#10;const var postInit = Content.addButton(&quot;postInit&quot;, 0, 0); //Hidden button to trigger last control callback on init&#10;postInit.set(&quot;visible&quot;, false);&#10;&#10;//Functions&#10;&#10;//Callbacks&#10;function onNoteOn()&#10;{&#10;&#9;local i;&#10;&#9;noteNumber = Message.getNoteNumber();&#10;&#10;&#9;if (cmbType.getValue() &gt; 1 &amp;&amp; noteNumber &gt;= lowNote &amp;&amp; noteNumber &lt;= highNote) //RR is not off&#10;&#9;{&#10;&#9;&#9;//Group based RR&#10;&#9;&#9;if (cmbType.getValue() == 2 || cmbType.getValue() == 4) //Real or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;if (numGroups != 1)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = (lastGroup.getValue(Message.getNoteNumber()) + 1) % numGroups;&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.TRUE_RANDOM:&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = Math.floor(Math.random() * numGroups);&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.RANDOM_NO_REPEAT:&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = -1;&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group == -1 || group == lastGroup.getValue(noteNumber))&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group = Math.floor(Math.random() * numGroups);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;&#9;case RRModes.RANDOM_FULL_CYCLE:&#10;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;&#9;if (groupCounter.getValue(noteNumber) &gt;= numGroups)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;groupCounter.setValue(noteNumber, 0); //Reset groupCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;for (gHistory in groupHistory)&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;gHistory.setValue(noteNumber, -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;&#9;group = -1;&#10;&#9;&#9;&#9;&#9;&#9;&#9;while (group == -1 || group == lastGroup.getValue(noteNumber) || groupHistory[group].getValue(noteNumber) != -1)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;group = Math.floor(Math.random() * numGroups);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;else &#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group = 0;&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset group if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(noteNumber) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;group = 0;&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;//Set active group for each samper&#10;&#9;&#9;&#9;for (sampler in samplers)&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;sampler.setActiveGroup(1+group);&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;groupCounter.setValue(noteNumber, groupCounter.getValue(noteNumber) + 1); //Increment counter&#10;&#9;&#9;&#9;groupHistory[group].setValue(noteNumber, 1); //Mark RR as used&#10;&#9;&#9;&#9;lastGroup.setValue(noteNumber, group);&#10;&#9;&#9;}&#10;&#9;&#9;&#10;&#9;&#9;//Synthetic sample borrowed RR&#10;&#9;&#9;if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR&#10;&#9;&#9;{&#10;&#9;&#9;&#9;offset = -1;&#10;&#10;&#9;&#9;&#9;switch (cmbMode.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;case RRModes.CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;offset = (lastOffset.getValue(noteNumber) + 1) % numOffsets;&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.TRUE_RANDOM:&#10;&#9;&#9;&#9;&#9;&#9;offset = Math.floor(Math.random() * numOffsets);&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.RANDOM_NO_REPEAT:&#10;&#10;&#9;&#9;&#9;&#9;&#9;while (offset == -1 || offset == lastOffset.getValue(noteNumber))&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset = Math.floor(Math.random() * numOffsets);&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;break;&#10;&#10;&#9;&#9;&#9;&#9;case RRModes.RANDOM_FULL_CYCLE:&#10;&#9;&#9;&#9;&#9;&#9;&#10;&#9;&#9;&#9;&#9;&#9;//Reset counter and history if all RRs have been played&#10;&#9;&#9;&#9;&#9;&#9;if (offsetCounter.getValue(noteNumber) &gt;= numOffsets)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offsetCounter.setValue(noteNumber, 0); //Reset offsetCounter for this note&#10;&#10;&#9;&#9;&#9;&#9;&#9;&#9;for (oHistory in offsetHistory)&#10;&#9;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;&#9;oHistory.setValue(noteNumber, -1);&#10;&#9;&#9;&#9;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;&#9;//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note&#10;&#9;&#9;&#9;&#9;&#9;while (offset == -1 || offset == lastOffset.getValue(noteNumber) || offsetHistory[offset].getValue(noteNumber) != -1)&#10;&#9;&#9;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;&#9;&#9;offset = Math.floor(Math.random() * numOffsets);&#10;&#9;&#9;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;&#9;break;&#10;&#9;&#9;&#9;}&#10;&#10;&#9;&#9;&#9;//Reset offset if timer expired&#10;&#9;&#9;&#9;if (Engine.getUptime() - timer.getValue(noteNumber) &gt; knbReset.getValue())&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;offset = 0;&#10;&#9;&#9;&#9;}&#10;&#9;&#9;&#9;&#10;&#9;&#9;&#9;if (noteNumber + offset - 1 &lt; lowNote) offset = 1;&#10;&#9;&#9;&#9;if (noteNumber + offset - 1 &gt; highNote) offset = 0;&#10;&#10;&#9;&#9;&#9;//Apply offset and coarse tuning to message&#10;&#9;&#9;&#9;Message.setTransposeAmount(offset-1); //Use -1 to make offset range -1 to +1&#10;&#9;&#9;&#9;Message.setCoarseDetune(-(offset-1)); //Use coarse detune so setting can be picked up by later scripts&#10;&#10;&#9;&#9;&#9;offsetCounter.setValue(noteNumber, offsetCounter.getValue(noteNumber) + 1); //Increment counter&#10;&#9;&#9;&#9;offsetHistory[offset].setValue(noteNumber, 1); //Mark RR as used&#10;&#9;&#9;&#9;lastOffset.setValue(noteNumber, offset);&#10;&#9;&#9;}&#10;&#10;&#9;&#9;//Apply random microtuning, if any&#10;&#9;&#9;if (knbMicroTuning.getValue() != 0)&#10;&#9;&#9;{&#10;&#9;&#9;&#9;Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue());&#10;&#9;&#9;}&#10;&#10;&#9;&#9;timer.setValue(noteNumber, Engine.getUptime());&#10;&#9;}&#10;}&#10;&#10;function onNoteOff()&#10;{&#10;&#9;&#10;}&#10;function onController()&#10;{&#10;&#9;&#10;}&#10;function onTimer()&#10;{&#10;&#9;&#10;}&#10;function onControl(number, value)&#10;{&#10;&#9;switch (number)&#10;&#9;{&#10;&#9;&#9;case knbLowNote:&#10;&#9;&#9;&#9;lowNote = value;&#10;&#9;&#9;break;&#10;&#10;&#9;&#9;case knbHighNote:&#10;&#9;&#9;&#9;highNote = value;&#10;&#9;&#9;break;&#10;&#10;&#9;&#9;case postInit:&#10;&#9;&#9;&#9;//Get number of groups - assumes all samplers have the same number of groups and samples are mapped over velocity 64&#10;&#9;&#9;&#9;numGroups = samplers.length != 0 ? samplers[0].getRRGroupsForMessage(lowNote + 1, 64) : 0;&#10;&#10;&#9;&#9;&#9;//Create group history MIDI lists&#10;&#9;&#9;&#9;for (i = 0; i &lt; numGroups; i++) //One MIDI list per group&#10;&#9;&#9;&#9;{&#10;&#9;&#9;&#9;&#9;groupHistory[i] = Engine.createMidiList();&#10;&#9;&#9;&#9;&#9;groupHistory[i].fill(-1);&#10;&#9;&#9;&#9;}&#10;&#9;&#9;break;&#10;&#9;}&#10;}">
            <EditorStates BodyShown="1" Visible="1" Solo="0" contentShown="1" onInitOpen="0"
                          onNoteOnOpen="0" onNoteOffOpen="0" onControllerOpen="0" onTimerOpen="0"
                          onControlOpen="0" Folded="1"/>
            <ChildProcessors/>
            <Content>
              <Control type="ScriptComboBox" id="Type" value="2"/>
              <Control type="ScriptComboBox" id="Mode" value="3"/>
              <Control type="ScriptSlider" id="Low Note" value="60"/>
              <Control type="ScriptSlider" id="High Note" value="96"/>
              <Control type="ScriptSlider" id="Reset Time" value="5"/>
              <Control type="ScriptSlider" id="Microtuning" value="0"/>
              <Control type="ScriptButton" id="postInit" value="0"/>
            </Content>
          </Processor>
          

          Libre Wave - Freedom respecting instruments and effects
          My Patreon - HISE tutorials
          YouTube Channel - Public HISE tutorials

          1 Reply Last reply Reply Quote 0
          • Christoph HartC
            Christoph Hart
            last edited by

            The postInit-thingie looks like a code smell to me. Why do you need it to happen after the onInit callback?

            1 Reply Last reply Reply Quote 0
            • d.healeyD
              d.healey
              last edited by d.healey

              Because it needs the value from lowNote and I believe (possibly mistakenly) that the value of the UI knobs isn't recalled until after onInit has completed. - now that's you've pointed it out I think I'm mixing it up with Kontakt's way of doing things.

              Libre Wave - Freedom respecting instruments and effects
              My Patreon - HISE tutorials
              YouTube Channel - Public HISE tutorials

              1 Reply Last reply Reply Quote 0
              • d.healeyD
                d.healey
                last edited by

                Version 2.4: I've added a knob for specifying the number of groups since I was having some issues doing it the other way.

                /**
                 * Multi-Round Robin script v2.4
                 * Author: David Healey
                 * Date: 07/01/2017
                 * Modified: 02/02/2017
                 * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html
                 */
                
                Content.setHeight(100);
                
                //Init
                const var excludedIds = "(elease)"; //Regex string of words in sampler IDs to exclude RR from
                const var samplerNames = Synth.getIdList("Sampler"); //Get the ids of all child samplers
                const var samplers = [];
                
                reg group; //Group RR counter
                reg offset; //Synthetic RR counter
                
                reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected
                
                const var lastGroup = Engine.createMidiList();
                const var lastOffset = Engine.createMidiList();
                
                const var groupCounter = Engine.createMidiList();
                const var offsetCounter = Engine.createMidiList();
                groupCounter.fill(0);
                offsetCounter.fill(0);
                
                var groupHistory = [];
                const var offsetHistory = [];
                
                reg lowNote;
                reg highNote;
                reg noteNumber;
                
                const var timer = Engine.createMidiList();
                
                namespace RRModes
                {
                	const var CYCLE = 1;
                	const var TRUE_RANDOM = 2;
                	const var RANDOM_NO_REPEAT = 3;
                	const var RANDOM_FULL_CYCLE = 4;
                };
                
                //Get child samplers
                for (samplerName in samplerNames)
                {
                	if (Engine.matchesRegex(samplerName, excludedIds) == true) continue; //Skip excluded IDs
                
                	samplers.push(Synth.getSampler(samplerName)); //Add sampler to array
                	samplers[samplers.length-1].enableRoundRobin(false); //Disable default RR behaviour
                	samplers[samplers.length-1].refreshRRMap();
                }
                
                //Create offset history MIDI lists - same is done for groupHistory in on control callback
                for (i = 0; i < numOffsets; i++) //3 possible offset states for synthetic RR
                {
                	offsetHistory[i] = Engine.createMidiList();
                	offsetHistory[i].fill(-1);
                }
                
                //GUI
                //RR Type and Mode Combo Boxes
                const var cmbType = Content.addComboBox("Type", 0, 10);
                cmbType.set("items", ["Off", "Real", "Synthetic", "Hybrid"].join("\n"));
                
                const var cmbMode = Content.addComboBox("Mode", 150, 10);
                cmbMode.set("items", ["Cycle", "Random", "Random No Repeat", "Random Full Cycle"].join("\n"));
                
                //Number of groups knob
                const var knbNumGroups = Content.addKnob("Num Groups", 300, 0);
                knbNumGroups.setRange(0, 100, 1);
                
                //Playable Range controls
                const var knbLowNote = Content.addKnob("Low Note", 450, 0);
                knbLowNote.setRange(0, 127, 1);
                
                const var knbHighNote = Content.addKnob("High Note", 600, 0);
                knbHighNote.setRange(0, 127, 1);
                
                //Reset timeout knob
                const var knbReset = Content.addKnob("Reset Time", 0, 50);
                knbReset.setRange(0, 60, 1);
                
                //Microtuning knob
                const var knbMicroTuning = Content.addKnob("Microtuning", 150, 50);
                knbMicroTuning.setRange(0, 100, 0.1);
                
                //Functions
                inline function createGroupHistoryMidiLists()
                {
                	groupHistory = []; //Reset variable
                
                	//Create group history MIDI lists
                	for (i = 0; i < knbNumGroups.getValue(); i++) //One MIDI list per group
                	{
                		groupHistory[i] = Engine.createMidiList();
                		groupHistory[i].clear();
                	}
                }
                
                //Callbacks
                function onNoteOn()
                {
                	local i;
                	noteNumber = Message.getNoteNumber();
                
                	if (cmbType.getValue() > 1 && noteNumber >= lowNote && noteNumber <= highNote) //RR is not off
                	{
                		//Group based RR
                		if (groupHistory.length > 0 && (cmbType.getValue() == 2 || cmbType.getValue() == 4)) //Real or Hybrid RR
                		{
                			if (knbNumGroups.getValue() != 1)
                			{
                				switch (cmbMode.getValue())
                				{
                					case RRModes.CYCLE:
                						group = (lastGroup.getValue(Message.getNoteNumber()) + 1) % parseInt(knbNumGroups.getValue());
                					break;
                
                					case RRModes.TRUE_RANDOM:
                						group = Math.floor(Math.random() * knbNumGroups.getValue());
                					break;
                
                					case RRModes.RANDOM_NO_REPEAT:
                
                						group = -1;
                						while (group == -1 || group == lastGroup.getValue(noteNumber))
                						{
                							group = Math.floor(Math.random() * knbNumGroups.getValue());
                						}
                						
                					break;
                
                					case RRModes.RANDOM_FULL_CYCLE:
                				
                						//Reset counter and history if all RRs have been played
                						if (groupCounter.getValue(noteNumber) >= knbNumGroups.getValue())
                						{
                							groupCounter.setValue(noteNumber, 0); //Reset groupCounter for this note
                
                							for (gHistory in groupHistory)
                							{
                								gHistory.setValue(noteNumber, -1);
                							}
                						}
                
                						//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note
                						group = -1;
                						while (group == -1 || group == lastGroup.getValue(noteNumber) || groupHistory[group].getValue(noteNumber) != -1)
                						{
                							group = Math.floor(Math.random() * knbNumGroups.getValue());
                						}
                					break;
                				}
                			}
                			else 
                			{
                				group = 0;
                			}
                
                			//Reset group if timer expired
                			if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue())
                			{
                				group = 0;
                			}
                			
                			//Set active group for each samper
                			for (sampler in samplers)
                			{
                				sampler.setActiveGroup(1+group);
                			}
                
                			groupCounter.setValue(noteNumber, groupCounter.getValue(noteNumber) + 1); //Increment counter
                			groupHistory[group].setValue(noteNumber, 1); //Mark RR as used
                			lastGroup.setValue(noteNumber, group);
                		}
                		
                		//Synthetic sample borrowed RR
                		if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR
                		{
                			offset = -1;
                
                			switch (cmbMode.getValue())
                			{
                				case RRModes.CYCLE:
                					offset = (lastOffset.getValue(noteNumber) + 1) % numOffsets;
                				break;
                
                				case RRModes.TRUE_RANDOM:
                					offset = Math.floor(Math.random() * numOffsets);
                				break;
                
                				case RRModes.RANDOM_NO_REPEAT:
                
                					while (offset == -1 || offset == lastOffset.getValue(noteNumber))
                					{
                						offset = Math.floor(Math.random() * numOffsets);
                					}
                
                				break;
                
                				case RRModes.RANDOM_FULL_CYCLE:
                					
                					//Reset counter and history if all RRs have been played
                					if (offsetCounter.getValue(noteNumber) >= numOffsets)
                					{
                						offsetCounter.setValue(noteNumber, 0); //Reset offsetCounter for this note
                
                						for (oHistory in offsetHistory)
                						{
                							oHistory.setValue(noteNumber, -1);
                						}
                					}
                
                					//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note
                					while (offset == -1 || offset == lastOffset.getValue(noteNumber) || offsetHistory[offset].getValue(noteNumber) != -1)
                					{
                						offset = Math.floor(Math.random() * numOffsets);
                					}
                
                				break;
                			}
                
                			//Reset offset if timer expired
                			if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue())
                			{
                				offset = 0;
                			}
                			
                			if (noteNumber + offset - 1 < lowNote) offset = 1;
                			if (noteNumber + offset - 1 > highNote) offset = 0;
                
                			//Apply offset and coarse tuning to message
                			Message.setTransposeAmount(offset-1); //Use -1 to make offset range -1 to +1
                			Message.setCoarseDetune(-(offset-1)); //Use coarse detune so setting can be picked up by later scripts
                
                			offsetCounter.setValue(noteNumber, offsetCounter.getValue(noteNumber) + 1); //Increment counter
                			offsetHistory[offset].setValue(noteNumber, 1); //Mark RR as used
                			lastOffset.setValue(noteNumber, offset);
                		}
                
                		//Apply random microtuning, if any
                		if (knbMicroTuning.getValue() != 0)
                		{
                			Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue());
                		}
                
                		timer.setValue(noteNumber, Engine.getUptime());
                	}
                }
                
                function onNoteOff()
                {
                }
                
                function onController()
                {
                }
                
                function onTimer()
                {	
                }
                
                function onControl(number, value)
                {
                	switch (number)
                	{
                		case knbLowNote:
                			lowNote = value;
                			createGroupHistoryMidiLists();
                		break;
                
                		case knbHighNote:
                			highNote = value;
                		break;
                	}
                }
                

                Libre Wave - Freedom respecting instruments and effects
                My Patreon - HISE tutorials
                YouTube Channel - Public HISE tutorials

                1 Reply Last reply Reply Quote 0
                • d.healeyD
                  d.healey
                  last edited by d.healey

                  A few more adjustments, one to correct an issue with setting high and low notes, and a couple of improvements to the mode selection and I set the minimum number of groups to 1 instead of 0.

                  /**
                   * Multi-Round Robin script v2.5
                   * Author: David Healey
                   * Date: 07/01/2017
                   * Modified: 09/02/2017
                   * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html
                   */
                  
                  Content.setHeight(100);
                  
                  //Init
                  const var excludedIds = "(elease)"; //Regex string of words in sampler IDs to exclude RR from
                  const var samplerNames = Synth.getIdList("Sampler"); //Get the ids of all child samplers
                  const var samplers = [];
                  
                  reg group; //Group RR counter
                  reg offset; //Synthetic RR counter
                  
                  reg numOffsets = 3; //Number of adjacent samples used by the synthetic RR (including 0) - currently anything other than 3 won't work as expected
                  
                  const var lastGroup = Engine.createMidiList();
                  const var lastOffset = Engine.createMidiList();
                  
                  const var groupCounter = Engine.createMidiList();
                  const var offsetCounter = Engine.createMidiList();
                  groupCounter.fill(0);
                  offsetCounter.fill(0);
                  
                  var groupHistory = [];
                  const var offsetHistory = [];
                  
                  reg lowNote;
                  reg highNote;
                  reg noteNumber;
                  
                  const var timer = Engine.createMidiList();
                  
                  namespace RRModes
                  {
                  	const var CYCLE = 1;
                  	const var TRUE_RANDOM = 2;
                  	const var RANDOM_NO_REPEAT = 3;
                  	const var RANDOM_FULL_CYCLE = 4;
                  };
                  
                  //Get child samplers
                  for (samplerName in samplerNames)
                  {
                  	if (Engine.matchesRegex(samplerName, excludedIds) == true) continue; //Skip excluded IDs
                  
                  	samplers.push(Synth.getSampler(samplerName)); //Add sampler to array
                  	samplers[samplers.length-1].enableRoundRobin(false); //Disable default RR behaviour
                  	samplers[samplers.length-1].refreshRRMap();
                  }
                  
                  //Create offset history MIDI lists - same is done for groupHistory in on control callback
                  for (i = 0; i < numOffsets; i++) //3 possible offset states for synthetic RR
                  {
                  	offsetHistory[i] = Engine.createMidiList();
                  	offsetHistory[i].fill(-1);
                  }
                  
                  //GUI
                  //RR Type and Mode Combo Boxes
                  const var cmbType = Content.addComboBox("Type", 0, 10);
                  cmbType.set("items", ["Off", "Real", "Synthetic", "Hybrid"].join("\n"));
                  
                  const var cmbMode = Content.addComboBox("Mode", 150, 10);
                  cmbMode.set("items", ["Cycle", "Random", "Random No Repeat", "Random Full Cycle"].join("\n"));
                  
                  //Number of groups knob
                  const var knbNumGroups = Content.addKnob("Num Groups", 300, 0);
                  knbNumGroups.setRange(1, 100, 1);
                  
                  //Playable Range controls
                  const var knbLowNote = Content.addKnob("Low Note", 450, 0);
                  knbLowNote.setRange(0, 126, 1);
                  
                  const var knbHighNote = Content.addKnob("High Note", 600, 0);
                  knbHighNote.setRange(1, 127, 1);
                  
                  //Reset timeout knob
                  const var knbReset = Content.addKnob("Reset Time", 0, 50);
                  knbReset.setRange(0, 60, 1);
                  
                  //Microtuning knob
                  const var knbMicroTuning = Content.addKnob("Microtuning", 150, 50);
                  knbMicroTuning.setRange(0, 100, 0.1);
                  
                  //Functions
                  inline function createGroupHistoryMidiLists()
                  {
                  	groupHistory = []; //Reset variable
                  
                  	//Create group history MIDI lists
                  	for (i = 0; i < knbNumGroups.getValue(); i++) //One MIDI list per group
                  	{
                  		groupHistory[i] = Engine.createMidiList();
                  		groupHistory[i].clear();
                  	}
                  }
                  
                  //Callbacks
                  function onNoteOn()
                  {
                  	local i;
                  	noteNumber = Message.getNoteNumber();
                  
                  	if (cmbType.getValue() > 1 && noteNumber >= lowNote && noteNumber <= highNote)
                  	{
                  		//Group based RR
                  		if (groupHistory.length > 0 && (cmbType.getValue() == 2 || cmbType.getValue() == 4)) //Real or Hybrid RR
                  		{
                  			if (knbNumGroups.getValue() != 1)
                  			{
                  				switch (cmbMode.getValue())
                  				{
                  					case RRModes.CYCLE:
                  						group = (lastGroup.getValue(Message.getNoteNumber()) + 1) % parseInt(knbNumGroups.getValue());
                  					break;
                  
                  					case RRModes.TRUE_RANDOM:
                  						group = Math.floor(Math.random() * knbNumGroups.getValue());
                  					break;
                  
                  					case RRModes.RANDOM_NO_REPEAT:
                  
                  						group = -1;
                  						while (group == -1 || group == lastGroup.getValue(noteNumber))
                  						{
                  							group = Math.floor(Math.random() * knbNumGroups.getValue());
                  						}
                  						
                  					break;
                  
                  					case RRModes.RANDOM_FULL_CYCLE:
                  				
                  						//Reset counter and history if all RRs have been played
                  						if (groupCounter.getValue(noteNumber) >= knbNumGroups.getValue())
                  						{
                  							groupCounter.setValue(noteNumber, 0); //Reset groupCounter for this note
                  
                  							for (gHistory in groupHistory)
                  							{
                  								gHistory.setValue(noteNumber, -1);
                  							}
                  						}
                  
                  						//Get random group number that isn't the last group and hasn't been marked as played in groupHistory for this note
                  						group = -1;
                  						while (group == -1 || group == lastGroup.getValue(noteNumber) || groupHistory[group].getValue(noteNumber) != -1)
                  						{
                  							group = Math.floor(Math.random() * knbNumGroups.getValue());
                  						}
                  					break;
                  				}
                  			}
                  			else 
                  			{
                  				group = 0;
                  			}
                  
                  			//Reset group if timer expired
                  			if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue())
                  			{
                  				group = 0;
                  			}
                  			
                  			//Set active group for each sampler
                  			for (sampler in samplers)
                  			{
                  				sampler.setActiveGroup(1+group);
                  			}
                  
                  			groupCounter.setValue(noteNumber, groupCounter.getValue(noteNumber) + 1); //Increment counter
                  			groupHistory[group].setValue(noteNumber, 1); //Mark RR as used
                  			lastGroup.setValue(noteNumber, group);
                  		}
                  		
                  		//Synthetic sample borrowed RR
                  		if (cmbType.getValue() == 3 || cmbType.getValue() == 4) //Synthetic or Hybrid RR
                  		{
                  			offset = -1;
                  
                  			switch (cmbMode.getValue())
                  			{
                  				case RRModes.CYCLE:
                  					offset = (lastOffset.getValue(noteNumber) + 1) % numOffsets;
                  				break;
                  
                  				case RRModes.TRUE_RANDOM:
                  					offset = Math.floor(Math.random() * numOffsets);
                  				break;
                  
                  				case RRModes.RANDOM_NO_REPEAT:
                  
                  					while (offset == -1 || offset == lastOffset.getValue(noteNumber))
                  					{
                  						offset = Math.floor(Math.random() * numOffsets);
                  					}
                  
                  				break;
                  
                  				case RRModes.RANDOM_FULL_CYCLE:
                  					
                  					//Reset counter and history if all RRs have been played
                  					if (offsetCounter.getValue(noteNumber) >= numOffsets)
                  					{
                  						offsetCounter.setValue(noteNumber, 0); //Reset offsetCounter for this note
                  
                  						for (oHistory in offsetHistory)
                  						{
                  							oHistory.setValue(noteNumber, -1);
                  						}
                  					}
                  
                  					//Get random offset number that isn't the last offset and hasn't been marked as played in offsetHistory for this note
                  					while (offset == -1 || offset == lastOffset.getValue(noteNumber) || offsetHistory[offset].getValue(noteNumber) != -1)
                  					{
                  						offset = Math.floor(Math.random() * numOffsets);
                  					}
                  
                  				break;
                  			}
                  
                  			//Reset offset if timer expired
                  			if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue())
                  			{
                  				offset = 0;
                  			}
                  			
                  			if (noteNumber + offset - 1 < lowNote) offset = 1;
                  			if (noteNumber + offset - 1 > highNote) offset = 0;
                  
                  			//Apply offset and coarse tuning to message
                  			Message.setTransposeAmount(offset-1); //Use -1 to make offset range -1 to +1
                  			Message.setCoarseDetune(-(offset-1)); //Use coarse detune so setting can be picked up by later scripts
                  
                  			offsetCounter.setValue(noteNumber, offsetCounter.getValue(noteNumber) + 1); //Increment counter
                  			offsetHistory[offset].setValue(noteNumber, 1); //Mark RR as used
                  			lastOffset.setValue(noteNumber, offset);
                  		}
                  
                  		//Apply random microtuning, if any
                  		if (knbMicroTuning.getValue() != 0)
                  		{
                  			Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue());
                  		}
                  
                  		timer.setValue(noteNumber, Engine.getUptime());
                  	}
                  }
                  
                  function onNoteOff()
                  {
                  }
                  
                  function onController()
                  {
                  }
                  
                  function onTimer()
                  {	
                  }
                  
                  function onControl(number, value)
                  {
                  	switch (number)
                  	{
                  		case cmbType:
                  			if (value == 2 && knbNumGroups.getValue() == 1) //Real but only one group
                  			{
                  				number.setValue(1); //Set mode to off
                  			}
                  		break;
                  
                  		case knbLowNote:
                  			lowNote = value;
                  			createGroupHistoryMidiLists();
                  		break;
                  
                  		case knbHighNote:
                  			highNote = value;
                  		break;
                  	}
                  }
                  

                  Libre Wave - Freedom respecting instruments and effects
                  My Patreon - HISE tutorials
                  YouTube Channel - Public HISE tutorials

                  1 Reply Last reply Reply Quote 0
                  • Christoph HartC
                    Christoph Hart
                    last edited by

                    Looks tasty. What's the performance of the onNoteOn callback (you can see the CPU amount of a single callback in the Script Watch Table if you enable the DBG button)? There are a lot of loops which might cause some spikes (especially in the RANDOM_FULL_CYCLE branch).

                    Here's another approach for full cycle random:

                    1.) Create a random order
                    2.) Cycle through it like normal round robin
                    3.) When you're through, shuffle the order and reset the index

                    This is a quick sketch that demonstrates this idea:

                    
                    /** Swaps two elements in a list randomly. */
                    inline function swapRandomly(list)
                    {
                    	local firstIndex = Math.randInt(0, list.length);
                    	local secondIndex = Math.randInt(0, list.length);
                    	local temp = list[firstIndex];
                    	list[firstIndex] = list[secondIndex];
                    	list[secondIndex] = temp;
                    }
                    
                    /** Swaps every element in the list. */
                    inline function swapEntirely(list)
                    {
                    	local i = 0;
                    	while(++i < list.length)
                    		swapRandomly(list);
                    }
                    
                    reg groupList = [1,2,3,4];
                    reg groupIndex;
                    
                    function onNoteOn()
                    {
                    	Console.print(groupList[groupIndex]);
                    	
                    	if(++groupIndex >= groupList.length)
                    	{
                    		groupIndex = 0;
                    		Console.print("Shuffle!");
                    		swapEntirely(groupList);
                    	}
                    }
                    function onNoteOff(){}
                    function onController(){}
                    function onTimer(){}
                    function onControl(number, value) {}
                    

                    You'll need to change the shuffle functions to support MidiLists, but my gut feeling tells me this is faster (and more predictable).

                    1 Reply Last reply Reply Quote 0
                    • d.healeyD
                      d.healey
                      last edited by

                      Thanks I'll explore your code - and thanks for the CPU tip, I didn't know you could profile it like that!

                      Libre Wave - Freedom respecting instruments and effects
                      My Patreon - HISE tutorials
                      YouTube Channel - Public HISE tutorials

                      1 Reply Last reply Reply Quote 0
                      • Christoph HartC
                        Christoph Hart
                        last edited by

                        Yes its pretty hard to check the script performance of a single callback by looking on the CPU meter :)

                        1 Reply Last reply Reply Quote 0
                        • d.healeyD
                          d.healey
                          last edited by d.healey

                          Version 3.0: Major revision.

                          • Incorporated Christoph's randomising system.
                          • Added namespace for RRTypes
                          • Removed random no repeat mode - I could see no point in keeping it when we have the random full cycle
                          • Aimed for simpler code and lower CPU usage so the RR is no longer per note it is for the whole keyboard. Reset timer is still per note though
                          • CPU usage of on note callback is now around 0.60 on average on my system (before it was running up to 3.5!)
                          • Added sample range knob for synth and hybrid modes. This is used to set how far from the played note the sample borrowed modes will take samples from. So setting it to 1 means the samples 1 down and one up from the played note will be used, as well as the actual note's sample.
                          /**
                           * Multi-Round Robin script v3.2
                           * Author: David Healey, Christoph Hart
                           * Date: 07/01/2017
                           * Modified: 20/02/2017
                           * License: GPLv3 - https://www.gnu.org/licenses/gpl-3.0.en.html
                           */
                          
                          //INIT
                          
                          reg noteNumber;
                          reg groupList = [];
                          reg groupIndex;
                          reg offsetList = [];
                          reg offsetIndex;
                          reg offset;
                          
                          namespace RRTypes
                          {
                          	const var OFF = 1;
                          	const var REAL = 2;
                          	const var SYNTH = 3;
                          	const var HYBRID = 4;
                          };
                          
                          namespace RRModes
                          {
                          	const var CYCLE = 1;
                          	const var RANDOM = 2;
                          	const var RANDOM_CYCLE = 3;
                          };
                          
                          const var timer = Engine.createMidiList();
                          
                          const var excludedIds = "(elease)"; //Regex string of words in sampler IDs to exclude RR from
                          const var samplerNames = Synth.getIdList("Sampler"); //Get the ids of all child samplers
                          const var samplers = [];
                          
                          //Get child samplers
                          for (samplerName in samplerNames)
                          {
                          	if (Engine.matchesRegex(samplerName, excludedIds) == true) continue; //Skip excluded IDs
                          
                          	samplers.push(Synth.getSampler(samplerName)); //Add sampler to array
                          	samplers[samplers.length-1].enableRoundRobin(false); //Disable default RR behaviour
                          }
                          
                          //GUI
                          Content.setHeight(100);
                          
                          //RR Type and Mode Combo Boxes
                          const var cmbType = Content.addComboBox("Type", 0, 10);
                          cmbType.set("items", ["Off", "Real", "Synthetic", "Hybrid"].join("\n"));
                          
                          const var cmbMode = Content.addComboBox("Mode", 150, 10);
                          cmbMode.set("items", ["Cycle", "Random", "Random Full Cycle"].join("\n"));
                          
                          //Number of groups knob
                          const var knbNumGroups = Content.addKnob("Num Groups", 300, 0);
                          knbNumGroups.setRange(1, 100, 1);
                          
                          //Playable Range controls
                          const var knbLowNote = Content.addKnob("Low Note", 450, 0);
                          knbLowNote.setRange(0, 126, 1);
                          
                          const var knbHighNote = Content.addKnob("High Note", 600, 0);
                          knbHighNote.setRange(1, 127, 1);
                          
                          //Reset timeout knob
                          const var knbReset = Content.addKnob("Reset Time", 0, 50);
                          knbReset.setRange(1, 60, 1);
                          knbReset.set("suffix", " Seconds");
                          
                          //Microtuning knob
                          const var knbMicroTuning = Content.addKnob("Microtuning", 150, 50);
                          knbMicroTuning.setRange(0, 100, 0.1);
                          
                          //Borrowed Sample Range
                          const var knbSampleRange = Content.addKnob("Sample Range", 300, 50);
                          knbSampleRange.setRange(1, 12, 1);
                          
                          //-----------------
                          
                          //FUNCTIONS
                          /** Swaps two elements in an array randomly. */
                          inline function swapRandomly(arr)
                          {
                          	local firstIndex = Math.randInt(0, arr.length);
                          	local secondIndex = Math.randInt(0, arr.length);
                          	local temp = arr[firstIndex];
                          	arr[firstIndex] = arr[secondIndex];
                          	arr[secondIndex] = temp;
                          }
                          
                          /** Swaps every element in the array. */
                          inline function swapEntirely(arr)
                          {
                          	local i = 0;
                          	while(++i < arr.length)
                          		swapRandomly(arr);
                          }
                          
                          inline function resetGroupList(count)
                          {
                          	local i;
                          
                          	for (i = 0; i < count; i++)
                          	{
                          		groupList[i] = i;
                          	}
                          }
                          
                          inline function resetOffsetList(count)
                          {
                          	local i;
                          
                          	//Build array going from -value to +value+1 (always need to account for 0)
                          	for (i = 0; i < count*2+1; i++)
                          	{
                          		offsetList[i] = i-count;
                          	}
                          }
                          
                          //CALLBACKS
                          function onNoteOn()
                          {
                          	noteNumber = Message.getNoteNumber();
                          
                          	if (cmbType.getValue() != RRTypes.OFF && noteNumber >= knbLowNote.getValue() && noteNumber <= knbHighNote.getValue())
                          	{
                          		//Group based RR
                          		if (knbNumGroups.getValue() > 1 && (cmbType.getValue() == RRTypes.REAL || cmbType.getValue() == RRTypes.HYBRID)) //Real or Hybrid RR
                          		{
                          			switch (cmbMode.getValue())
                          			{
                          				case RRModes.CYCLE:
                          					groupIndex = (groupIndex + 1) % groupList.length;
                          				break;
                          
                          				case RRModes.RANDOM:
                          					groupIndex = Math.floor(Math.random() * groupList.length);
                          				break;
                          
                          				case RRModes.RANDOM_CYCLE:
                          					if (++groupIndex >= groupList.length)
                          					{
                          						groupIndex = 0;
                          						swapEntirely(groupList);
                          					}
                          				break;
                          			}
                          		}
                          		
                          		//Reset groupIndex to 0 if only one group is present or the reset time has elapsed
                          		if (knbNumGroups.getValue() == 1 || Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue())
                          		{
                          			groupIndex = 0;
                          			resetGroupList(knbNumGroups.getValue());
                          		}
                          
                          		//Set active group for each sampler
                          		if (samplers.length > 0)
                          		{
                          			for (sampler in samplers)
                          			{
                          				sampler.setActiveGroup(1 + groupList[groupIndex]);
                          			}
                          		}
                          
                          		//Synthetic sample borrowed RR
                          		if (cmbType.getValue() == RRTypes.SYNTH || cmbType.getValue() == RRTypes.HYBRID) //Synthetic or Hybrid RR
                          		{
                          			switch (cmbMode.getValue())
                          			{
                          				case RRModes.CYCLE:
                          					offsetIndex = (offsetIndex + 1) % offsetList.length;
                          				break;
                          
                          				case RRModes.RANDOM:
                          					offsetIndex = Math.floor(Math.random() * offsetList.length);
                          				break;
                          
                          				case RRModes.RANDOM_CYCLE:
                          					if (++offsetIndex >= offsetList.length)
                          					{
                          						offsetIndex = 0;
                          						swapEntirely(offsetList);
                          					}
                          				break;
                          			}
                          			
                          			//If reset time has elapsed, reset index
                          			if (Engine.getUptime() - timer.getValue(noteNumber) > knbReset.getValue())
                          			{
                          				offsetIndex = parseInt(Math.floor(offsetList.length / 2));
                          				resetOffsetList(knbSampleRange.getValue());
                          			}
                          
                          			//If the played note + the offset are within the playable range
                          			if (noteNumber + offsetList[offsetIndex] > knbLowNote.getValue() && noteNumber + offsetList[offsetIndex] < knbHighNote.getValue())
                          			{
                          				Message.setTransposeAmount(offsetList[offsetIndex]); //Transpose the note
                          				Message.setCoarseDetune(-offsetList[offsetIndex]); //Use coarse detune so setting can be picked up by later scripts
                          			}
                          		}
                          
                          		//Apply random microtuning, if any
                          		if (knbMicroTuning.getValue() > 0)
                          		{
                          			Message.setFineDetune(Math.random() * (knbMicroTuning.getValue() - -knbMicroTuning.getValue() + 1) + -knbMicroTuning.getValue());
                          		}
                          
                          		timer.setValue(noteNumber, Engine.getUptime()); //Record time this note was triggered
                          	}
                          }
                          
                          function onNoteOff()
                          {
                          }
                          
                          function onController()
                          {
                          }
                          
                          function onTimer()
                          {
                          }
                          
                          function onControl(number, value)
                          {
                          	switch(number)
                          	{
                          		case knbNumGroups:
                          			groupList = [];
                          			resetGroupList(value);
                          		break;
                          
                          		case knbSampleRange:
                          			offsetList = [];
                          			resetOffsetList(value);
                          		break;
                          
                          		case cmbType:
                          
                          			switch (value)
                          			{
                          				case RRTypes.REAL:
                          					
                          					knbSampleRange.set("visible", false);
                          
                          					if (knbNumGroups.getValue() == 1)
                          					{
                          						number.setValue(RRTypes.OFF);
                          					}
                          				break;
                          
                          				case RRTypes.SYNTH:
                          					knbSampleRange.set("visible", true);
                          				break;
                          
                          				case RRTypes.HYBRID:
                          					
                          					knbSampleRange.set("visible", true);
                          
                          					if (knbNumGroups.getValue() == 1) //If there are no RR groups then Hybrid defaults to synth
                          					{
                          						number.setValue(RRTypes.SYNTH);
                          					}
                          				break;
                          			}
                          
                          		break;
                          
                          		case cmbMode:
                          			resetGroupList(knbNumGroups.getValue());
                          			resetOffsetList(knbSampleRange.getValue());
                          			offsetIndex = 0;
                          			groupIndex = 0;
                          		break;
                          	}
                          }
                          

                          Libre Wave - Freedom respecting instruments and effects
                          My Patreon - HISE tutorials
                          YouTube Channel - Public HISE tutorials

                          1 Reply Last reply Reply Quote 2
                          • First post
                            Last post

                          29

                          Online

                          1.8k

                          Users

                          12.0k

                          Topics

                          104.1k

                          Posts