HISE Logo Forum
    • Categories
    • Register
    • Login

    Random, Cycle, Synthetic, Hybrid Round Robin

    Scheduled Pinned Locked Moved Presets / Scripts / Ideas
    13 Posts 2 Posters 3.1k 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

      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

                        47

                        Online

                        1.7k

                        Users

                        11.7k

                        Topics

                        101.8k

                        Posts