VCV Prototype: Share your prototypes

It might be useful to encourage script sharing with VCV Prototype, since it’s easy to share your module prototypes. You’ll first need VCV Prototype to create or load scripts.

How to share patches

There are many ways, such as uploading the script itself as an attachment, but I find this to be the easiest method:

  • Load your script in VCV Prototype.
  • Right click on the panel and select Preset > Copy, or hover your mouse over the panel and use Ctrl-C (Cmd on Mac).
  • Paste this text into this thread as a code block, by surrounding the text by triple backticks (```) on their own line. You may omit the "params" object if you want.

How to load patches

  • Highlight and copy the module preset.
  • Hover over an empty rack space, or an existing VCV Prototype module, and press Ctrl-V (Cmd on Mac).
3 Likes

Factorial router

Inspired from Module to Switch Cable Routes.

This uses input 6 as a CV control to choose the permutation number. The CV is split into 120 intervals, each corresponding to a permutation of inputs 1 through 5.

{
  "plugin": "VCV-Prototype",
  "version": "1.1.0",
  "model": "Prototype",
  "data": {
    "path": "/home/vortico/src/vcv/Rack-v1/plugins/VCV-Prototype/examples/factorial_router.js",
    "script": "\nfunction permutation(arr, k) {\n\tvar n = arr.length\n\tvar factorial = 1\n\tfor (var i = 2; i <= n; i++) {\n\t\tfactorial *= i\n\t}\n\tvar perm = []\n\n\tfor (var i = n; i >= 1; i--) {\n\t\tfactorial /= i\n\t\tvar j = Math.floor(k / factorial)\n\t\tk %= factorial\n\t\tvar el = arr[j]\n\t\tarr.splice(j, 1)\n\t\tperm.push(el)\n\t}\n\n\treturn perm\n}\n\nconfig.bufferSize = 32\n\nfunction process(block) {\n\t// Get factorial index from input 6\n\tvar k = Math.floor(block.inputs[5][0] / 10 * 120)\n\tk = Math.min(Math.max(k, 0), 120 - 1)\n\t// Get index set permutation\n\tvar perm = permutation([0, 1, 2, 3, 4], k)\n\tdisplay(perm)\n\tfor (var i = 0; i < 5; i++) {\n\t\t// Permute input i\n\t\tfor (var j = 0; j < block.bufferSize; j++) {\n\t\t\tblock.outputs[i][j] = block.inputs[perm[i]][j]\n\t\t}\n\t}\n}\n"
  }
}
1 Like

Here is an example file were you could see that the module or the script does not work as it should.Factorial Switch 5!.vcv (9.2 KB)

Output 1 sents nothing, all values seem been shifted by one down,
reading the script for me it seems a mix of values for 6! and 5!?

Knobs 1 - 5 set the values from 1 to 5, knob 6 is used to switch

1 Like

Two indexed buffers intended for use as phase driven loop recorders. Coming soon as my first proper module.

  "id": 3139,
  "plugin": "VCV-Prototype",
  "version": "1.1.0",
  "model": "Prototype",
  "params": [
    {
      "id": 0,
      "value": 0.5
    },
    {
      "id": 1,
      "value": 0.5
    },
    {
      "id": 2,
      "value": 0.5
    },
    {
      "id": 3,
      "value": 0.5
    },
    {
      "id": 4,
      "value": 0.5
    },
    {
      "id": 5,
      "value": 0.5
    },
    {
      "id": 6,
      "value": 0.0
    },
    {
      "id": 7,
      "value": 0.0
    },
    {
      "id": 8,
      "value": 0.0
    },
    {
      "id": 9,
      "value": 0.0
    },
    {
      "id": 10,
      "value": 0.0
    },
    {
      "id": 11,
      "value": 0.0
    }
  ],
  "leftModuleId": 3143,
  "rightModuleId": 3141,
  "data": {
    "path": "/home/ewen/.Rack/PhaseLooper.js",
    "script": "// PhaseLooper.js (C)2019 Ewen Bates\n\n\n\nconfig.frameDivider = 16\nconfig.bufferSize = 1\n\nvar samplesA = new Float32Array(1000)\nvar samplesB = new Float32Array(1000)\n\nvar visuals = [\"+---------\", \"-+--------\", \"--+-------\", \"---+------\", \"----+-----\", \"-----+----\", \"------+---\", \"-------+--\", \"--------+-\", \"---------+\"]\n\nfunction process(args)\n{\n    var phaseA = args.inputs[0][0]\n    var cvA = args.inputs[1][0]\n    var writeA = args.inputs[2][0]\n    var idxA = Math.floor(phaseA * 100)\n\n    var phaseB = args.inputs[3][0]\n    var cvB = args.inputs[4][0]\n    var writeB = args.inputs[5][0]\n    var idxB = Math.floor(phaseB * 100)\n\n    if(writeA) samplesA[idxA] = cvA\n    if(writeB) samplesB[idxB] = cvB\n\n    if(args.switches[2]) zeroArray(samplesA)\n    if(args.switches[5]) zeroArray(samplesB)\n\n    args.outputs[0][0] = samplesA[idxA]\n    args.outputs[1][0] = cvA\n    args.outputs[2][0] = writeA\n    args.outputs[3][0] = samplesB[idxB]\n    args.outputs[4][0] = cvB\n    args.outputs[5][0] = writeB\n\n    args.switchLights[0][1] = phaseA / 10\n    args.switchLights[1][2] = samplesA[idxA] / 10\n    args.switchLights[2][0] = writeA\n\n    args.switchLights[3][1] = phaseB / 10\n    args.switchLights[4][2] = samplesB[idxB] / 10\n    args.switchLights[5][0] = writeB\n\n    display(visuals[Math.floor(phaseA)] + \"  \" + visuals[Math.floor(phaseB)] + \"\\n\" + idxA + \" \" + idxB)\n}\n\nfunction zeroArray(array)\n{\n    for(i = 0; i < 1000; i++) array[i] = 0\n}\n"
  }
}```
1 Like

Thanks, fixed indexing typo in Factorial Router.

Step sequencer that does random walks/Brownian motions on each step’s value. (My first attempt at a Prototype script so may contain some bugs…)

edit : Updated the code and some documentation :

CV input 1 takes in external clock. (There’s no internal clock yet.)
CV input 2 takes in a pulse that resets the sequencer.

CV output 1 is the step sequencer value.

Knob 1 : Random walk probability distribution parameter 1
Knob 2 : Random walk probability distribution parameter 2
Knob 3 : Random walk probability distribution type :

  1. Hyperbolic cosine (close to a bell/Gaussian distribution), knob 1 controls the mean, knob 2 controls the spread
  2. Cauchy (also bell like, but can make huge jumps even with a very small knob 2 value), knob 1 controls the mean, knob 2 controls the spread
  3. Discrete distribution that always jumps down or up. Knob 1 controls the probability of downward or upward jump. Knob 2 controls the jump size.

Knob 6 : Number of steps played back, between 1 and 16.

Switch button 1 : Resets the sequencer
Switch button 2 : Switches between the sequencer reset types :

  1. All steps at zero
  2. All steps at minimum (-2.0 volts)
  3. All steps at maximum (2.0 volts)
  4. All steps randomized with a uniform random distribution between -2.0 and 2.0 volts
{
  "id": 24,
  "plugin": "VCV-Prototype",
  "version": "1.1.0",
  "model": "Prototype",
  "params": [
    {
      "id": 0,
      "value": 0.5
    },
    {
      "id": 1,
      "value": 0.0924999341
    },
    {
      "id": 2,
      "value": 0.0
    },
    {
      "id": 3,
      "value": 0.5
    },
    {
      "id": 4,
      "value": 0.5
    },
    {
      "id": 5,
      "value": 0.271500021
    },
    {
      "id": 6,
      "value": 0.0
    },
    {
      "id": 7,
      "value": 0.0
    },
    {
      "id": 8,
      "value": 0.0
    },
    {
      "id": 9,
      "value": 0.0
    },
    {
      "id": 10,
      "value": 0.0
    },
    {
      "id": 11,
      "value": 0.0
    }
  ],
  "data": {
    "path": "C:\\MusicAudio\\vcv\\mysequencer1.js",
    "script": "config.frameDivider = 32\nconfig.bufferSize = 1\ncounter = 0\nstepon = false\noutvolt = 0.0\nhasreset = false\nresetmode = 0\nresetmodechanged = false\noutvoltmin = -2.0\noutvoltmax = 2.0\n\nvar steps = [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]\n\npi = 3.141592653\n\nfunction hypcosrand(mean,dev)\n{\n\tvar z = Math.random()\n\treturn mean+dev*(2/pi*Math.log(Math.tan(pi/2.0*z)))\n}\n\nfunction cauchyrand(mean,dev)\n{\n\tvar z = Math.random()\n\treturn mean + dev * Math.tan(pi*(z-0.5))\n}\n\nfunction bernouillirand(prob, step)\n{\n\tvar z = Math.random()\n\tif (z>prob)\n\t\treturn -step\n\treturn step\n}\n\nfunction clamp(val,minv,maxv)\n{\n\tif (val<minv)\n\t\treturn minv\n\tif (val>maxv)\n\t\treturn maxv\n\treturn val\n}\n\nfunction reflect(val,minv,maxv)\n{\n\tvar temp = val\n\twhile (temp<minv || temp>maxv)\n\t{\n\t\tif (temp<minv)\n\t\t\ttemp = minv + (minv - temp);\n\t\tif (temp>maxv)\n\t\t\ttemp = maxv + (maxv - temp);\n\t}\n\treturn temp\n}\n\nfunction walkarray(arr,minv,maxv,par1,par2,par3)\n{\n\tfor (var i=0;i<arr.length;++i)\n\t{\n\t\tval = arr[i]\n\t\tif (par3 == 0)\n\t\t{\n\t\t\tval += hypcosrand(-1.0+2.0*par1,par2)\n\t\t}\n\t\tif (par3 == 1)\n\t\t{\n\t\t\tval += cauchyrand(-1.0+2.0*par1,par2)\t\n\t\t}\n\t\tif (par3 == 2)\n\t\t{\n\t\t\tval += bernouillirand(par1,par2)\n\t\t}\n\t\tarr[i] = reflect(val,minv,maxv)\n\t}\n}\n\nfunction reset_steps(arr,mode)\n{\n\tfor (var i=0;i<arr.length;++i)\n\t{\n\t\tif (mode == 0)\n\t\t\tarr[i] = 0.0\n\t\tif (mode == 1)\n\t\t\tarr[i] = outvoltmin\n\t\tif (mode == 2)\n\t\t\tarr[i] = outvoltmax\n\t\tif (mode == 3)\n\t\t\tarr[i] = outvoltmin+(outvoltmax-outvoltmin)*Math.random()\n\t}\n}\n\nfunction process(block)\n{\n\tif (block.inputs[0][0]>0.5 && stepon==false) // step clock\n\t{\n\t\toutvolt = steps[counter]\n\t\tcounter++\n\t\tif (counter>=1.0+15.0*block.knobs[5])\n\t\t{\n\t\t\tcounter = 0\n\t\t\twalkarray(steps,outvoltmin,outvoltmax,block.knobs[0],block.knobs[1],Math.round(block.knobs[2]*2))\n\t\t}\n\t\tstepon = true\n\t}\n\tif (block.inputs[0][0]<0.5 && stepon==true)\n\t{\n\t\tstepon = false\n\t}\n\tif (block.inputs[1][0]>0.5 || block.switches[0]>0.5) // reset\n\t{\n\t\treset_steps(steps,resetmode)\n\t}\n\tif (block.switches[1]>0.5 && resetmodechanged == false)\n\t{\n\t\tresetmode = (resetmode+1) % 4\n\t\tresetmodechanged = true\n\t}\n\tif (block.switches[1]<0.5 && resetmodechanged == true)\n\t{\n\t\tresetmodechanged = false\n\t}\n\tdisplay(resetmode)\n\tblock.outputs[0][0]=outvolt\n}\n"
  }
}
3 Likes

Chord

Converts a single input on Input 0 to either a major or minor chord on Outputs 0, 1, 2

Pressing Button 1 changes from major to minor.

Use Merge to convert the 3 outputs into a single poly cable, or use 3 oscillators, your choice!

{
  "id": 156,
  "plugin": "VCV-Prototype",
  "version": "1.1.1",
  "model": "Prototype",
  "params": [
    {
      "id": 0,
      "value": 0.5
    },
    {
      "id": 1,
      "value": 0.5
    },
    {
      "id": 2,
      "value": 0.5
    },
    {
      "id": 3,
      "value": 0.5
    },
    {
      "id": 4,
      "value": 0.5
    },
    {
      "id": 5,
      "value": 0.5
    },
    {
      "id": 6,
      "value": 0.0
    },
    {
      "id": 7,
      "value": 0.0
    },
    {
      "id": 8,
      "value": 0.0
    },
    {
      "id": 9,
      "value": 0.0
    },
    {
      "id": 10,
      "value": 0.0
    },
    {
      "id": 11,
      "value": 0.0
    }
  ],
  "leftModuleId": 155,
  "data": {
    "path": "/Users/jerry/Documents/Rack/plugins-v1/VCV-Prototype/examples/chord.js",
    "script": "config.frameDivider = 32;\nconfig.bufferSize = 1;\n\nconst step = 1.0 / 12.0;\nconst MAJOR = 4;\nconst MINOR = 3;\n\n\ndisplay('Chord');\n\n// CV module, based on SynthDevKit CV module\nfunction CV(threshold) {\n  this.threshold = threshold;\n  this.reset();\n}\n\nCV.prototype.update = function update(current) {\n  this.lastValue = current;\n\n  this.triggerIntervalCount++;\n\n  if (current >= this.threshold) {\n    if (this.triggered === false) {\n      this.triggered = true;\n\n      this.triggerCount++;\n\n      this.lastTriggerInterval = this.triggerIntervalCount;\n\n      this.triggerIntervalCount = 0;\n    }\n  } else {\n    this.triggered = false;\n  }\n};\n\nCV.prototype.newTrigger = function newTrigger() {\n  if (this.triggered === true && this.lastTriggered === false) {\n    this.lastTriggered = true;\n\n    return true;\n  }\n\n  this.lastTriggered = this.triggered;\n\n  return false;\n};\n\nCV.prototype.reset = function reset() {\n  this.triggered = false;\n  this.lastTriggered = false;\n  this.lastValue = 0;\n  this.triggerCount = 0;\n  this.triggerIntervalCount = 0;\n  this.lastTriggerInterval = 0;\n};\n\nCV.prototype.currentValue = function() {\n  return this.lastValue;\n};\n\nCV.prototype.isLow = function isLow() {\n  return !this.triggered;\n};\n\nCV.prototype.isHigh = function isHigh() {\n  return this.triggered;\n};\n\nCV.prototype.triggerInterval = function() {\n  return this.lastTriggerInterval;\n};\n\nCV.prototype.triggerTotal = function() {\n  return this.triggerCount;\n};\n\nlet cvState = new CV(1);\nlet switchValue = 0;\n\nfunction process(block) {\n  let voct = block.inputs[0][0];\n\n  cvState.update(block.switches[0]);\n  if (cvState.newTrigger()) {\n    switchValue = !switchValue;\n  }\n\n  var middle = switchValue ? MINOR : MAJOR;\n\n  display('Chorus - ' + (switchValue ? 'Minor' : 'Major'));\n\n  block.outputs[0][0] = voct;\n  block.outputs[1][0] = voct + (step * middle);\n  block.outputs[2][0] = voct + (step * 7);\n}\n"
  }
}
2 Likes

experimenting with Math.min and Math.max to make a comparator:
Input 1 = comparator in1
input 2 = comparator in2
input 3 = biasCV
knob 1 = bias
output 1 = max
output 2 = min
output 3 = mix (influenced by bias)

{
  "plugin": "VCV-Prototype",
  "version": "1.1.1",
  "model": "Prototype",
  "data": {
    "path": "~/logic.js",
    "script": "config.frameDivider = 1\nconfig.bufferSize = 16\n\nvar phase = 0\nvar amp = 1\nvar mix;\nvar bias_cv\nlet clampNumber = (num, a, b) => Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));\n\n\nfunction process(block) {\n    // block.knobs[0] = 0.5\n\t// Knob ranges from -5 to 5 octaves\n\t// var pitch = block.knobs[0] * 10 - 5\n\t// Input follows 1V/oct standard\n    // pitch += block.inputs[0][0]\n    \n    amp = block.inputs[1][0]\n\n    compare = [block.inputs[0][0], block.inputs[1][0]]\n    max = Math.max(...compare)\n    min = Math.min(...compare)\n\n    // let i = compare.indexOf(Math.max(...compare));\n\n    function indexOfMax(arr) {\n        if (arr.length === 0) {\n            return -1;\n        }\n        var max = arr[0];\n        var maxIndex = 0;\n        for (var i = 1; i < arr.length; i++) {\n            if (arr[i] > max) {\n                maxIndex = i;\n                max = arr[i];\n            }\n        }\n        return maxIndex;\n    }\n\n    var bias = block.knobs[0] * 2 - 1\n    // bias_cv += bias * Math.pow(2, block.inputs[2][0]);\n    if(bias < 0.){\n        mix = clampNumber( ((Math.abs(bias) * min) + (bias * max) * max * block.inputs[2][0]), -10., 10.)\n    } else if (bias > 0.){\n        mix = clampNumber( ((bias * max) + ((bias * -1) * min) * min * block.inputs[2][0]), -10., 10.)\n    }\n\n    var freq = 1\n\t//display(\"Freq: \" + freq.toFixed(3) + \" Hz\")\n\n\t// Set all samples in output buffer\n\tvar deltaPhase = config.frameDivider * block.sampleTime * freq\n\tfor (var i = 0; i < block.bufferSize; i++) {\n\t\t// Accumulate phase\n\t\tphase += deltaPhase\n\t\t// Wrap phase around range [0, 1]\n        phase %= 1\n        // display([bias, bias_cv, mix])\n        \n\t\t// Convert phase to sine output\n        // block.outputs[0][i] = Math.sin(2 * Math.PI * phase) * 5 * amp\n        block.outputs[0][i] = max\n        block.outputs[1][i] = min\n        block.outputs[2][i] = mix\n\t}\n}\n"
  }
}
2 Likes

Gradual keyboard split

Cross-fades from one audio source to another depending on which note you play.

There are three inputs, all using V/Oct, and one output in the range -5 to 5. Inputs 1 and 2 indicate where the fade starts and ends. (Connect to Bogaudio’s Reftone module.) Connect input 3 to V/Oct indicating the note being played on the keyboard. The output indicates how to crossfade the audio from left to right. (Connect to Bogaudio’s XFADE.)

One of the LED’s will light up indicating the approximate output voltage, left LED is -5 and right LED is 5.

Alternately, you could use Formula using something like: (w-(x+y)/2)*10/(y-x) where x and y are the split points.

{
  "id": 152,
  "plugin": "VCV-Prototype",
  "version": "1.2.0",
  "model": "Prototype",
  "params": [
    {
      "id": 0,
      "value": 0.5
    },
    {
      "id": 1,
      "value": 0.5
    },
    {
      "id": 2,
      "value": 0.5
    },
    {
      "id": 3,
      "value": 0.5
    },
    {
      "id": 4,
      "value": 0.5
    },
    {
      "id": 5,
      "value": 0.5
    },
    {
      "id": 6,
      "value": 0.0
    },
    {
      "id": 7,
      "value": 0.0
    },
    {
      "id": 8,
      "value": 0.0
    },
    {
      "id": 9,
      "value": 0.0
    },
    {
      "id": 10,
      "value": 0.0
    },
    {
      "id": 11,
      "value": 0.0
    }
  ],
  "leftModuleId": 153,
  "rightModuleId": 160,
  "data": {
    "path": "/Users/skybrian/Documents/Rack/scripts/split.js",
    "script": "/*\n  Gradual keyboard split.\n  Cross-fades from one audio source to another depending on which note you play.\n  \n  There are three inputs, all using V/Oct, and one output in the range -5 to 5.\n  Inputs 1 and 2 indicate where the fade starts and ends. (Connect to Bogaudio's Reftone module.)\n  Connect input 3 to V/Oct indicating the note being played on the keyboard.\n  The output indicates how to crossfade the audio from left and right. (Connect to Bogaudio's XFADE.)\n */\nfunction process(block) {\n    var out = 0;\n\tlet vOct = block.inputs[0][0];\n\tlet splitStart = block.inputs[1][0];\n\tlet splitEnd = block.inputs[2][0];\n\tif (splitStart == splitEnd) {\n\t\tif (vOct < splitStart) {\n            out = -5;\n\t\t} else {\n\t\t\tout = 5;\n\t\t}\n\t} else {\n        let zeroIntercept = (splitEnd + splitStart) / 2.0;\n        let slope = 10.0 / (splitEnd - splitStart);\n        out = (vOct - zeroIntercept) * slope;\n        if (out < -5) {\n            out = -5;\n        } else if (out > 5) {\n            out = 5;\n        }\n    }\n    // display(out)\n    block.outputs[0][0] = out;\n    let lit = Math.round((out + 5)/10*5.99-.5)\n    for (let i = 0; i < 6; i++) {\n        if (i == lit) {\n            block.lights[i][1] = 1; \n        } else {\n            block.lights[i][1] = 0; \n        }\n    }\n}\n"
  }
}
2 Likes

I was looking for a good idea to try out the prototype module. So I made the Morphing Triangle Oscillator that Finetales suggested

I’m no DSP programmer. I’m barely a programmer. So it’s all a little hackey. Especially the S&H. But it can actually get pretty sonically interesting. I haven’t made any real modules yet, but this one will probably get ported if I do.

Knob & In 1: V/Octave
Knob & In 2: Skew
Knob & In 3: S&H
Knob & In 4: Diet
In 5: Hard Sync
In 6: FM
{
  "id": 69,
  "plugin": "VCV-Prototype",
  "version": "1.2.0",
  "model": "Prototype",
  "data": {
    "path": "/Users/nateallen/Documents/Rack/plugins-v1/VCV-Prototype/examples/trimorph.js",
    "script": "// Morphing Triangle Oscillator\n// Designed by u/Finetales\n// Programmed by Nate Allen\n\n// Code modified from the Voltage-controlled oscillator example by Andrew Belt\n\n\n// Knob & In 1: V/Octave\n// Knob & In 2: Skew\n// Knob & In 3: S&H\n// Knob & In 4: Diet\n// In 5: Hard Sync\n// In 6: FM\n\nconfig.frameDivider = 1\nconfig.bufferSize = 16\n\nlet phase = 0\n\n// SKEW CONSTANTS\n// The max slope when skewing peaks left and right.\n// We never technically get to a true sawtooth, but we can get close\nconst maxSlope = 50\n\n\n// STEP CONSTANTS\n// When the step knob is at zero, this is ignored.\n// This sets the maximum phase steps when you do turn the knob\nconst maxPrecision = 100\n// This set the exponential ramp through the step settings\nconst exp = 10\n// Since we apply the step knob exponentially,\n// there's a point where we don't actually get a cycling\n// waveform anymore. There's a single sample through the whole\n// cycle. This calculates that point, and we'll not let our values\n// go past that.\nconst maxAmount = 1-(Math.pow(1/maxPrecision, 1/exp))\n\n// DIET CONSTANTS\n// Set the max exponent for the diet\nconst maxCurve = 8\n\n// Used to tell whether our sync oscillator has crossed the zero point\nlet syncPositive = false\n\nfunction process(block) {\n\t// Knob ranges from -5 to 5 octaves\n\tlet pitch = block.knobs[0] * 10 - 5\n\t// Input follows 1V/oct standard\n\t// Take the first input's first buffer value\n\tpitch += block.inputs[0][0]\n\n\tlet skew = block.knobs[1]\n\tskew += block.inputs[1][0] / 10\n\t// skew\n\tlet slope = Math.pow(maxSlope, skew * 2 - 1) + 1\n\tlet pairedSlope = 1 / (slope-1) + 1\n\n\tlet step = block.knobs[2]\n\tstep += block.inputs[2][0] / 10\n\n\tlet diet = block.knobs[3]\n\tdiet += block.inputs[3][0] / 10\n\tlet curve = Math.pow(maxCurve, diet * 2 - 1)\n\n\tif(syncPositive && block.inputs[4][0] < 0) {\n\t\tsyncPositive = false\n\t}\n\n\tif(!syncPositive && block.inputs[4][0] > 0) {\n\t\tsyncPositive = true\n\t\tphase = 0\n\t}\n\n\t// The relationship between 1V/oct pitch and frequency is `freq = 2^pitch`.\n\t// Default frequency is middle C (C4) in Hz.\n\t// https://vcvrack.com/manual/VoltageStandards.html#pitch-and-frequencies\n\tlet freq = 261.6256 * Math.pow(2, pitch)\n\tdisplay(\"Freq: \" + freq.toFixed(3) + \" Hz / Skew:\"\n\t\t+ skew + \" / Step: \" + step + \" / diet: \" + diet)\n\n\t// Set all samples in output buffer\n\tlet deltaPhase = config.frameDivider * block.sampleTime * freq\n\tfor (let i = 0; i < block.bufferSize; i++) {\n\t\t// Accumulate phase\n\t\tphase += deltaPhase\n\t\t// Wrap phase around range [0, 1]\n\t\tphase %= 1\n\n\t\tlet y = sampleAndHold(phase, step)\n\n\t\t// Basic FM\n\t\ty += block.inputs[5][0] / 10 + 0.5\n\t\ty %= 1\n\n\t\ty *= slope\n\n\t\tif (y > 1)\n\t\t\ty = pairedSlope * (slope - y) / slope\n\n\t\t// waveshaper based on diet\n\t\ty = Math.pow(y, curve)\n\n\t\tblock.outputs[0][i] = y * 10 - 5\n\t}\n}\n\nfunction sampleAndHold(phase, amount){\n\tif(amount <= 0)\n\t\treturn phase\n\tamount *= maxAmount\n\tprecision = maxPrecision * Math.pow(1-amount, exp)\n\treturn Math.round(phase * precision) / precision\n}\n"
  }
}

Edit - So, I realized that this is just Tides, aka Audible Instruments tidal modulator 2. So, this was fun to build, but you get all of this functionality from that module, and it’s much less CPU intensive since it’s not a prototype.

6 Likes

Headphones crossfeed plugin, Jan Meier style

This helps with listening to records from the '60s and '70s on headphones (Beatles, Pink Floyd etc.), see the docs included in the script for usage. It uses mid/side encoding, a short delay and two RC filters. The CPU usage stays below 5% on my notebook (and will get even lower once this PR is merged).

Here’s a nice block diagram:

┌───────────────────────────────────────────────────────────────────────┐
│                                                                       │
│       ┌────────────┐                              ┌────────────┐      │
│  ────>│            ├─────────────────────────────>│            ├───>  │
│  L    │  Mid/side  │ M  ┌────────┐              M │  Mid/side  │  L   │
│  R    │  encoder   │ S  │ Hipass │         g    S │  decoder   │  R   │
│  ────>│            ├───>│ 100 Hz ├──┬─────>+─────>│            ├───>  │
│       └────────────┘    │ 1 pole │  │      A      └────────────┘      │
│        a                └────────┘  │      │       h                  │
│                          b          │      │                          │
│                                     │      │                          │
│                         ┌────────┐  │   ┌─────┐                       │
│                         │ Delay  │<─┘   │ vol │                       │
│                         │ 300 uS │      └─────┘                       │
│                         └────────┘       f A                          │
│                          c   │             │                          │
│                              V             │                          │
│                         ┌────────┐         │                          │
│                         │ Lopass │      ┌─────┐                       │
│                         │ 2 KHz  ├─────>│ inv │                       │
│                         │ 1 pole │      └─────┘                       │
│                         └────────┘       e                            │
│                          d                                            │
│                                                                       │
└───────────────────────────────────────────────────────────────────────┘

And here’s the script (first one in Lua!). EDIT: updated to v. 1.1:

{
  "id": 122,
  "plugin": "VCV-Prototype",
  "version": "1.2.0",
  "model": "Prototype",
  "data": {
    "path": "/home/nl/devel/mine/vcvrack/prototype/crossfeed.lua",
    "script": "-- # Headphones crossfeed\n--\n--   v. 1.1 - 20200524\n--   Nicola \"teknico\" Larosa <vcvrack@teknico.net>\n--\n-- Better headphones listening of hard-panned stereo music ('60s, '70s).\n--\n-- See http://www.meier-audio.homepage.t-online.de/crossfeed.htm\n--\n-- ## Usage\n--\n-- IN1 and OUT1 are left channels, IN2 and OUT2 are right channels.\n--\n-- * K1 knob: crossfeed amount (default: equal)\n-- * K2 knob: stereo width (default: normal)\n-- * S1 switch: complete bypass (default: off)\n-- * S2 switch: disable mono lows (default: off)\n--\n-- ## Algorithm\n--\n-- The stereo input is encoded to mid and side signals. The mid signal is sent\n-- as-is to the mid/side decoder. The side signal is subject to two changes:\n--\n-- * low frequencies are removed, resulting in them being mono in the output,\n--   in keeping with the human inability to detect their origin;\n-- * the remaining side signal is delayed according to the distance between\n--   ears, some high frequencies are removed to simulate the human head\n--   filtering effect, the signal is inverted and the result is summed back to\n--   the original side signal, resulting in cross components being added to\n--   each channel, as described in the link at the top.\n--\n-- The mid signal is left alone to minimize coloration due to comb filtering,\n-- and because the mono mid signal does not need additional cross components.\n--\n-- For mid/side encoding and decoding see:\n-- https://en.wikipedia.org/wiki/Joint_encoding#M/S_stereo_coding and\n-- https://en.wikipedia.org/wiki/Stereophonic_sound#M%2FS_technique%3A_mid%2Fside_stereophony\n--\n--\n-- ┌───────────────────────────────────────────────────────────────────────┐\n-- │                                                                       │\n-- │       ┌────────────┐                              ┌────────────┐      │\n-- │  ────>│            ├─────────────────────────────>│            ├───>  │\n-- │  L    │  Mid/side  │ M  ┌────────┐              M │  Mid/side  │  L   │\n-- │  R    │  encoder   │ S  │ Hipass │         g    S │  decoder   │  R   │\n-- │  ────>│            ├───>│ 100 Hz ├──┬─────>+─────>│            ├───>  │\n-- │       └────────────┘    │ 1 pole │  │      A      └────────────┘      │\n-- │        a                └────────┘  │      │       h                  │\n-- │                          b          │      │                          │\n-- │                                     │      │                          │\n-- │                         ┌────────┐  │   ┌─────┐                       │\n-- │                         │ Delay  │<─┘   │ vol │                       │\n-- │                         │ 300 uS │      └─────┘                       │\n-- │                         └────────┘       f A                          │\n-- │                          c   │             │                          │\n-- │                              V             │                          │\n-- │                         ┌────────┐         │                          │\n-- │                         │ Lopass │      ┌─────┐                       │\n-- │                         │ 2 KHz  ├─────>│ inv │                       │\n-- │                         │ 1 pole │      └─────┘                       │\n-- │                         └────────┘       e                            │\n-- │                          d                                            │\n-- │                                                                       │\n-- └───────────────────────────────────────────────────────────────────────┘\n\n\n-- Delay class.\n\nlocal Delay = {}\nDelay.__index = Delay\n\nsetmetatable(Delay, {\n    __call = function (cls, ...)\n        return cls.new(...)\n    end,\n})\n\nfunction Delay.new(size)\n    local self = setmetatable({}, Delay)\n    self.size = size\n    self.buffer = {}\n    -- Init the ring buffer so that values may be used right away.\n    for i=1,size do self.buffer[i] = 0 end\n    self.cur = 1\n    return self\nend\n\nfunction Delay.next(self, val)\n    local ret = self.buffer[self.cur]\n    self.buffer[self.cur] = val\n    -- No brackets needed with 1-based indexing.\n    self.cur = self.cur % self.size + 1\n    return ret\nend\n\n\n-- RCFilter class, based on include/dsp/filter.hpp in VCV Rack.\n\nlocal RCFilter = {}\nRCFilter.__index = RCFilter\n\nsetmetatable(RCFilter, {\n    __call = function (cls, ...)\n        return cls.new(...)\n    end,\n})\n\nfunction RCFilter.new(freq, fs)\n    local self = setmetatable({}, RCFilter)\n    local cutoff = fs / (math.pi * freq)\n    self.minus = 1.0 - cutoff\n    self.plus = 1.0 + cutoff\n    self.x = 0\n    self.y = 0\n    return self\nend\n\nfunction RCFilter.next(self, x)\n    -- Return:\n    --   lowpass, hipass\n    local y = (x + self.x - self.y * self.minus) / self.plus\n    self.x = x\n    self.y = y\n    return y, (x - y)\nend\n\nfunction calc_output(left_input, right_input, ctx, params)\n    -- Args:\n    --   left_input, right_input: left and right current values\n    --   ctx: see the make_context function\n    --   params: see the handle_ui function\n    --\n    -- Return:\n    --   left_output, right_output\n\n    -- Bypass (not shown on diagram.)\n    if params.bypass then\n        return left_input, right_input\n    end\n\n    -- (a) compute mid and side signals, keeping the overall level constant.\n    local mid = (left_input + right_input) / 2.0\n    local side = (left_input - right_input) / 2.0\n\n    -- (b) hipass-filter the side signal if not disabled.\n    local hi_side\n    if params.no_mono_lows then\n        hi_side = side\n    else\n        _, hi_side = ctx.HIPASS:next(side)\n    end\n\n    -- (c) delay the signal from (b).\n    local other_side\n    other_side = ctx.DELAY:next(hi_side)\n\n    -- (d) lowpass-filter the signal from (c).\n    other_side, _ = ctx.LOWPASS:next(other_side)\n\n    -- (e, f) invert and attenuate the signal from (d).\n    other_side = -other_side * params.xfeed_amount\n\n    -- (g) sum the signal from (b) and the one from (f).\n    local out_side\n    out_side = hi_side + other_side\n\n    -- Apply the width control (not shown on the diagram).\n    out_side = out_side * params.width\n\n    -- (h) mid/side decode the mid signal and the one from above.\n    return mid + out_side, mid - out_side\nend\n\nfunction handle_ui(block, ctx)\n    -- Get knobs and switches values, turn on and off lights.\n    --\n    -- Args:\n    --   block: the VCV Rack Prototype environment\n    --   ctx: see the make_context function\n    --\n    -- Return:\n    --   params:\n    --     crossfeed amount (left knob), 0..2\n    --     stereo width (right knob), 0..2\n    --     complete bypass (left switch), boolean\n    --     no mono lows (right switch), boolean\n\n    -- Bypass.\n    if block.switches[ctx.LEFT_IDX] then\n        -- Green switch light off.\n        block.switchLights[ctx.LEFT_IDX][2] = 0\n        display(\"BYPASS\")\n        return {\n            xfeed_amount = 0,\n            width = 0,\n            bypass = true,\n            no_mono_lows = false,\n        }\n    end\n\n    local params = {}\n\n    -- No bypass, green switch light on.\n    block.switchLights[ctx.LEFT_IDX][2] = 1\n    params.xfeed_amount = block.knobs[ctx.LEFT_IDX] * 2\n    params.width = block.knobs[ctx.RIGHT_IDX] * 2\n    local msg = string.format(\n        \"xfeed %.2f, width %.2f\", params.xfeed_amount, params.width)\n\n    params.no_mono_lows = block.switches[ctx.RIGHT_IDX]\n    if params.no_mono_lows then\n        -- Blue switch light off.\n        block.switchLights[ctx.RIGHT_IDX][3] = 0\n    else\n        -- Mono lows, blue switch light on.\n        block.switchLights[ctx.RIGHT_IDX][3] = 1\n        msg = string.format(\"%s, mono lows\", msg)\n    end\n    display(msg)\n\n    params.bypass = false\n    return params\nend\n\nfunction make_context(block)\n    -- Make a context table with the fields:\n    --\n    --   LEFT_IDX, RIGHT_IDX: indexes of the left and right UI columns\n    --   FS: current sampling frequency\n    --   DELAY: short delay unit\n    --   HIPASS, LOWPASS: hipass and lowpass filters, initialized with the\n    --     current sampling frequency\n\n    local ctx = {\n        LEFT_IDX = 1,\n        RIGHT_IDX = 2,\n        FS = block.sampleRate,\n        -- Approx. 300 uS.\n        DELAY = Delay(16),\n        HIPASS = RCFilter(100.0, block.sampleRate),\n        LOWPASS = RCFilter(2000.0, block.sampleRate),\n    }\n    return ctx\nend\n\n\n-- VCV Rack config parameters.\n\nconfig.frameDivider = 1\nconfig.bufferSize = 16\n\n\n-- Common context, see the make_context function.\nlocal ctx = nil\n\nfunction process(block)\n    if not ctx then\n        ctx = make_context(block)\n    end\n\n    -- The user interface doesn't need to be handled at audio rate...\n    local params = handle_ui(block, ctx)\n\n    -- ...the audio clearly does.\n    for j=1,block.bufferSize do\n\n        -- Get the left and right inputs.\n        local left_input = block.inputs[ctx.LEFT_IDX][j]\n        local right_input = block.inputs[ctx.RIGHT_IDX][j]\n\n        -- Compute the left and right outputs.\n        left_output, right_output = calc_output(\n            left_input, right_input, ctx, params)\n\n            -- Set the left and right outputs.\n        block.outputs[ctx.LEFT_IDX][j] = left_output\n        block.outputs[ctx.RIGHT_IDX][j] = right_output\n    end\nend\n"
  }
}

Enjoy!

1 Like