XY Pad with Optional Motion + Speed Control (HISE Script)
-
Hey everyone,
After my question yesterday I wanted to share a the script I’ve been working on. Would love to hear thoughts on updates as to whether this is the most sensible approach. Particular mention should go to @David-Healey whose original XY Pad videos sent me down the rabbit whole with this and did a lot of the heavy lifting!
Overall it’s an XY Pad that supports:
- Manual user control via mouse or XY knobs
- Optional motion driven by a simple sine-wave “LFO”
- Adjustable motion speed
- Fully modular and easy to extend
The idea is that you can move the XY pad automatically, but still allow the user to take manual control at any time.
Features
- btnMotion: toggle automatic motion on/off
- knbMotionSpeed: adjust the speed of the motion
- Motion is applied as a small offset on top of the manual X/Y position
- Gains are mapped from the XY pad to two gain modules, with scaling
How it works
- baseX/baseY store the manual (user) position
- lfoX/lfoY store motion offsets generated by the LFO
- updateXY() combines base + motion values and clamps them 0–1
- LFO speed is multiplied by motionSpeed for easy adjustment
/// =============================== /// XY PAD STATE /// =============================== // Manual (user) position reg baseX = 0.5; reg baseY = 0.5; // Motion offsets reg lfoX = 0.0; reg lfoY = 0.0; // Motion enable reg motionEnabled = false; /// Speed reg motionSpeed = 1.0; /// =============================== /// COMPONENTS /// =============================== const var xypad = Content.getComponent("xypad"); const var btnMotion = Content.getComponent("btnMotion"); const var knbMotionSpeed = Content.getComponent("knbMotionSpeed"); const SIZE = 10; /// =============================== /// GAINS /// =============================== const gains1 = [ Synth.getEffect("Simple Gain1"), Synth.getEffect("Simple Gain2") ]; /// =============================== /// XY PAD CORE CALLBACK /// =============================== xypad.setControlCallback(onxypadControl); inline function onxypadControl(component, value) { local x = component.data.x; local y = component.data.y; local gainX = -50 + (65 * x); local gainY = -50 + (65 * (1 - y)); gains1[0].setAttribute(gains1[0].GainValue, gainX); gains1[1].setAttribute(gains1[1].GainValue, gainY); knbxy[0].setValue(x); knbxy[1].setValue(y); component.repaint(); } /// =============================== /// PAINT ROUTINE /// =============================== xypad.setPaintRoutine(function(g) { g.fillAll(this.get("bgColour")); var x = Math.range(this.data.x * this.getWidth(), 0, this.getWidth() - SIZE); var y = Math.range(this.data.y * this.getHeight(), 0, this.getHeight() - SIZE); g.setColour(this.get("itemColour")); g.fillEllipse([x, y, SIZE, SIZE]); }); /// =============================== /// CENTRAL XY UPDATE /// =============================== inline function updateXY() { local modX = motionEnabled ? lfoX : 0.0; local modY = motionEnabled ? lfoY : 0.0; xypad.data.x = Math.range(baseX + modX, 0, 1); xypad.data.y = Math.range(baseY + modY, 0, 1); xypad.changed(); } /// =============================== /// MOUSE INPUT /// =============================== xypad.setMouseCallback(function(event) { if (event.clicked || event.drag) { baseX = Math.range(event.x / this.getWidth(), 0, 1); baseY = Math.range(event.y / this.getHeight(), 0, 1); updateXY(); } }); /// =============================== /// XY KNOBS /// =============================== const knbxy = []; for (i = 0; i < 2; i++) { knbxy.push(Content.getComponent("knbxy" + i)); knbxy[i].setControlCallback(onknbxyControl); } inline function onknbxyControl(component, value) { baseX = knbxy[0].getValue(); baseY = knbxy[1].getValue(); updateXY(); } /// =============================== /// MOTION BUTTON /// =============================== inline function onbtnMotionControl(component, value) { motionEnabled = value; }; Content.getComponent("btnMotion").setControlCallback(onbtnMotionControl); /// =============================== /// SPEED CONTROL /// =============================== inline function onknbMotionSpeedControl(component, value) { motionSpeed = value; }; Content.getComponent("knbMotionSpeed").setControlCallback(onknbMotionSpeedControl); /// =============================== /// LFO ENGINE /// =============================== const LFO_RATE_X = 0.35; const LFO_RATE_Y = 0.25; const LFO_DEPTH = 0.25; reg phaseX = 0.0; reg phaseY = 0.0; xypad.startTimer(30); xypad.setTimerCallback(function() { phaseX += LFO_RATE_X * motionSpeed * 0.03; phaseY += LFO_RATE_Y * motionSpeed * 0.03; if (phaseX > 1.0) phaseX -= 1.0; if (phaseY > 1.0) phaseY -= 1.0; var waveX = Math.sin(phaseX * Math.PI * 2); var waveY = Math.sin(phaseY * Math.PI * 2); lfoX = waveX * LFO_DEPTH * 0.5; lfoY = waveY * LFO_DEPTH * 0.5; updateXY(); }); -
any reason you used sin twice with extra logic rather than cos?
-
I think something like this should do about the same.
/// =============================== /// LFO ENGINE /// =============================== const LFO_RATE = 0.3; const LFO_DEPTH = 0.25; const TIME_SCALE = 0.03; reg phase = 0.0; xypad.startTimer(30); xypad.setTimerCallback(function() { phase = (phase + LFO_RATE * motionSpeed * TIME_SCALE) % 1.0; const angle = phase * Math.PI * 2; lfoX = Math.sin(angle) * LFO_DEPTH * 0.5; lfoY = Math.cos(angle) * LFO_DEPTH * 0.5; updateXY(); }); -
@xm4ddy Thanks for this I will have to drop it in and see how it works. Plenty to learn still!
-
@JamesC or... instead of coding up your own sine based LFO, you could just use an LFO, and in your timer query its value, then you get all the LFO shapes, (including hand drawn) can set the timer to synced tempo, have retrigger etc....
Here's a product that does just that:
https://www.tracktion.com/products/atmosia
Check the walkthru 1 video at 6:15 onwards for a demo...
-
You can achieve all of this relatively easily with the modulation system. Here is a snippet for you. The snippet is a little more complex because I am controlling the volume of four layers with one XY pad.
If it's too complicated and not immediately understandable, I can build you a simpler snippet tomorrow. Just let me know... :-)
In the end, this can be achieved with relatively little code, and you don't need a timer or anything like that. You can also use this method to control your pad with all the modulators that HISE has to offer. :-)
HiseSnippet 3261.3oc6bs0aabbEdojnikRSQbaJRQ.Jv.ihhkITzjTT4hTcrrDkrIhtPXJaKEifjg6Njbg1cF1cWJRlF2l+JEnn+G5SwE8OP6C887TeNOkGa5YlYWtyRsjhVxVV1lDvzbOy4Ly24xblyLCop5xLHddLWsTKre+1DsT+rz05S8asQKrEUqRYsTuY5d82gYtOwyWa89swddDSsTol8NbFRM+bZhW+vsVGaioFjHRZZOfYYP11xwxOhZ009LKa6svlj8sbT3tzZULXzMX1rN.XlMcds1XiivMI6h4rMSZs6h8Zok58SmujA9iK1X4kV5S9jOZYy5DiFEH0+HCb9kwMVFCTKgKsbgRZotxllV9L2Z9XehmVp4VmY1uVKVWpb.dfkmUcaB+gBZ0fQVRdKlsIWE4T01nkksY0PqjmF.3pQ1rYk1r2I8NVlVCnGY6daQCnHITMfolIN7lMF7JnBu7JvKAHkRARyIgz0RWyv0pseTKReYEpOwsAF7SpPQxq1L+8qkdCFvA0OmC9HxVtvCCjP+CymOKBdKypKr.3q77QGicQMAk0CcSjHrIWSh+sss2rQChgum90qY4z1lf3wJWOtXGyr+LJqNWxGs.BdENvPOrAyoMiBOnecduW35YxdJrT7zYYoSmkRWOyBeQLTVESI1E.LlrPxl4ZVjHGLRtOHNiGNRFOjynKoIxqEqisIL6qiMDi.7W.P2B23Fnal7KdS6fa21h1bwMA2hOw1tCsIgNNYhPzNU18KKuNLLKVHe9b4WMVSkkMAss7PMc6CBZBgDRwGr0IVn5V1VTB1k.7X4YzBPBxyhZhrnHSAQePov17.DRWKiVvS4yUb4UFZbeHB5bdCPWaQ48IpQGpguEihLrwNs0ONKxlkE0xJyB+QgO1k32wkB1BHjzwhp2xJavC3d5bVONCXhe7H5u7EzOdndZv3.ZXVTgb4khCZZ.zQ54ykqPFzheJxbcjtvBBbgfjevTDWL02h35OPqqC1GvRBr1kPont2jqem.McIVMa4uOqbc8st+tazMDT1LCrMpKXVBwqrYv.wa1pARuK52yMZ.BFXLDd2UU33SuoPSh3P3IC5DP0pZQL.syifjNxUBa.527bMkqkQzJtLmFWsBoUHfOdjgJxjt0LBRRMJRq7AspK5FRVVU1y4JLfoXJC5CP5AQlKFPJC58Q9Rk7wh2I1djQNP.XVL.LvPpC3cvyiYnKGNzxH+ECHEanSH1xi3uMtOwE7l17+uB0jzKKh65LqG20ZVOz2pKaNaf5kMvIkIxKcm6WAgOxuC1FVvf3RnHcVKXb2.lYUGVCcwZswNRacXR2GEM9eQN.VO.a2gnCfHpWKWqJGveMu6TxhmYfazxqLoAnel5hU.T6xLC6Yg9qBeNOE4PrfrRTzV+y+JLumhNxFRW4iLwdqLf6SzebHdaeeWq5c7Imb3xwAVVTD7CFxs.CfGLpbmvcqTayEe.w0CbD2XqC3CHhJl25falE9niCxCVH12DVsjPWQseNE7jOXnGoeuSaSH6sv06AKo5bvgBmZucYtNR2ee9GiGAHZcnI3BZAijjq9IvU+.tB8jhAdwOuCy0jBofGLId6BqfX0Ae.f1i7xh1tXvytDvtvIrzJnN70mFvQoPBRVTyEwWjLX9iDlvjgfm6eBX2sHm6.K7Dv9RI04Ahm.6kRr2Gheg.JSIA2nR51tExDzmJrTHNKESfkhwYYoDXYo3rTJyfkRFyp5Gb3hUwlnOm.wrzwudtrnDdLZUHv0+drN9P3ndX3ndyv.M95qXvT42xxiWAx1bq25rNTSO87gVnl7NRVVtt7+7xU2FRqDnXMy0.pm+dP4d53wIS2VV9jAxX5h6JkAVLMRLdWsossUaOh9iDvBl4fy0C76E.+SDk9ATJJ92WvsfYVcBsg6f63MY1ucXc7HgoQiLfjigpzBMh7TgBB4LrsLNhXh9luAII.JYygyDppT2LfudqlPy8GzbekrZbWVucijDVzJz68PKS+V5YVMFu8i3suBu2UD+oqlu7fn0A5sqRubXD896pJPBY05saVTeUgEimKoMONTePJxgralrNv1b1fa8Fu0ZHMERhgJNJS2vZpj2jz174VNY0U1v3zWfCdAgKepZL+8.sNPoawNl3JU24OQU94WMdkKymz9.lj.dPqODUy1hWsMTFFO5+CP2WnHiSzgW.iQOfuQEWlstQ3tTfRn4VonoA5wAopqb9foTC7jBQgbxAzGN5c9Dr0BQxB9mlg9mLJq4dfLii.iClvFA6LIryAF8vmcJU+DUpnoYiRqNPQcBG7Hs5vj0pCUzpwG.TgZ4uhbGGSPFuAtmS3VPfZAw4qtvv57I00PNOwLrSLyZ7SnB5YkoRKnjYle1QtCLIR5wnkjGOA1z4t54GdR6iiFJerqbvzKwWRTI5YWlOYOprC.IPC2TiFI1VfuyF5wjZVNViQPcZGm5DWkHUAiZolK9oIkdzmlj5gcYHO.BEFYTdTydsIzQcDXZAmZglVpqDfJfUew4N8yCN2IYRGMKw4ncfl.rQG122eKsIRvCOaB9FoEGajpv4+wGWZwmL4BW77H7RpB2p1+9er7SgvkRP36WoLLiieReAldvczl35aw8zoJSN1xfHO2u4SWl3cjOqMzcCNQI3yxA8sBOUPdzsXLuZZYjtVuHir0Z8UenKOGfJgVho5pTfYSrtgyo.H8doEKmCaY3t704P+NTYnVnlVzl7fmgQ0HhYTPDUAQe+WulIoAtisLYhZrgCF1SVu8wtP9nJhdomFT0oS3oJO+Uzz9oFBlmPTb3nPwZ+4mBTzeXT7SmDEKbpQy8TOZcEOTk0bTOI9CehCNFqNLyf.CCq5fa2wxzzlTk4YwyrDwYtmzNbRNG1.Rh11eAs1XWrCwmHZ6JoEm9+SC9Kphey0tfwewyM9WRE+9Wz3eoyM9Kof+u8wWz3uTx3+j2awamNnVq3WiBm8fF3io5cWvueBJfk9pWF0yr6VYRg30RW0x2nUxXblDvHjL+4AFCtQp2Js7JXh.3bo25fmOW+j5veU4v+douiMqN1NxPAKaAHgHuFpeSPynAsiFvPgwbIi+qI8RFaOwWx3vkMM2jU1zobGgyco6NBmz33e4IbMdWfy1RMiBLulDloSu8V6EXofOEArBmNvV2pMyF6xaeKWxenCgZD19W0nw+8V7HjJT0noGhOlzf45DTM0fdZaRSXLUorOAx8VqO0PU7ZNLFraBZy3wcZZayXs2jhAqfoZmTsE1iuUAXKMw5Fna2msC1CxWtgMy3H0FqzjxbIxsenReiNd9LmPEPbk407IsEUNl5Qo+vR4fWM9.96exFvaKs2ylmeZRjUXxc+iOJkGILvkyiItDtlfDkCB7dAhxSapTwy0TIs+xd+uoSkdcdpzbSVP53W07BcZ0EJhGawgy9pcwgxqep4NhMECof1siSMn5KCBL5TJwleTUolgOmS9bd9yhItDpo3geBdEzXA9yoBZrPXiZxNO3HRlAJkjRLj6M4MheX.6H1Jib7jnPbgtgC4vdhu59+mu6V2tSu.lFDM8cBZI30pPOl+k7PXB0FIN5ONbjJAb.u9amcbnD.7tx.f2LMO0l3Krkv8+qRGlpCcGBTC9Iqt54dc36Y3CPXeWL0qMyiTHVJbhi09v1b8hQcHIJlnDwnVl32gFuqkjJN7xVaAlBE9t5ZgDKpRrJlFqyfmi0S6X0S8bgjqYUi.Zt4ddFfggami0Acr8HhibufpfQjKpR9tXWygWzL0rOO1NyUdoc6Lu.O4fXeqLmODixyDYSX9oMCnvw3uHcY4IIFRM9TucXTV6VLpULG88HPJulMItpXOQE5199X0pddm0tGwl.0JEQ52t11hueWxzRmIaQgm5ugpI5u90okvEwyWfd48Ded0dQ8XtyqN36MtHz9NgC+Pme7nVM4OUZwmTlXiiYndX7qb3v0RX8F4ZbU4aJYXy7rOqBamahiE3pz4Jd8BsfTdJGvj+xDfEgDu7.32leuZWBf6EQ42SrIAJH5xcN82MMTXVQjHwN5ESl8KXG1SwNBJdYYGA+v2McGAS2QvqS6Hn3zcDLcGAuZsifhS2QvK530mE6B3B.jm+J+u.VT47Vs+KMELNsB+WcqveoKMU3eqoU3OsB+WmpveooU3OsB+Wspvejql7s7eSCSqveZE9SqveZE9Sqv+hrB+RWZ9V8r1zJ7mVg+qSU3WZZE9Sqv+UqJ7KMsB+oU3OsB+oU3OsB+mCNrKhwvAa3x9RC4eaJ34FupfBn2Twe1cmO8N7mQEF9ODGZ7eF3VeogQ7t5DBV7rJ3RmUAKcVEb4ypfe3YUvO5rJ3Ge5BxqW+1c7YNx4Fv9Vpto72uVpA+34RMq1+2DSycn