Forum
    • Categories
    • Register
    • Login

    XY Pad with Optional Motion + Speed Control (HISE Script)

    Scheduled Pinned Locked Moved Scripting
    6 Posts 4 Posters 83 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.
    • J
      JamesC
      last edited by

      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();
      });
      
      LindonL Oli UllmannO 2 Replies Last reply Reply Quote 1
      • X
        xm4ddy
        last edited by

        any reason you used sin twice with extra logic rather than cos?

        X 1 Reply Last reply Reply Quote 0
        • X
          xm4ddy @xm4ddy
          last edited by

          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();
          });
          
          J 1 Reply Last reply Reply Quote 0
          • J
            JamesC @xm4ddy
            last edited by

            @xm4ddy Thanks for this I will have to drop it in and see how it works. Plenty to learn still!

            1 Reply Last reply Reply Quote 0
            • LindonL
              Lindon @JamesC
              last edited by Lindon

              @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...

              HISE Development for hire.
              www.channelrobot.com

              1 Reply Last reply Reply Quote 0
              • Oli UllmannO
                Oli Ullmann @JamesC
                last edited by

                @JamesC

                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
                
                1 Reply Last reply Reply Quote 1
                • First post
                  Last post

                32

                Online

                2.1k

                Users

                13.2k

                Topics

                114.6k

                Posts