From 62d4f84e698abcfddc33d65d62f5a640472f7bf4 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 18 Jun 2026 10:55:34 +0100 Subject: [PATCH 1/5] add growlist --- packages/core/pattern.mjs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index bf9d8ce0b..76568fd2f 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2028,9 +2028,9 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f const newWhole = !hap.whole ? undefined : new TimeSpan( - newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), - newPart.end.add(hap.whole.end.sub(end).div(factor)), - ); + newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), + newPart.end.add(hap.whole.end.sub(end).div(factor)), + ); return new Hap(newWhole, newPart, hap.value, hap.context); }; return pat.withQuerySpanMaybe(qf).withHap(ef).splitQueries(); @@ -3359,6 +3359,11 @@ Pattern.prototype.shrinklist = function (amount) { export const shrinklist = (amount, pat) => pat.shrinklist(amount); +Pattern.prototype.growlist = function (amount) { + return this.shrinklist(amount).reverse(); +} +export const growlist = (amount, pat) => pat.growlist(amount); + /** * *Experimental* * From 813cc6c1904f73f4f2afd373681a3a5d16f613cd Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 18 Jun 2026 12:04:03 +0100 Subject: [PATCH 2/5] format --- packages/core/pattern.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 76568fd2f..ff808ac4a 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2028,9 +2028,9 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f const newWhole = !hap.whole ? undefined : new TimeSpan( - newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), - newPart.end.add(hap.whole.end.sub(end).div(factor)), - ); + newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), + newPart.end.add(hap.whole.end.sub(end).div(factor)), + ); return new Hap(newWhole, newPart, hap.value, hap.context); }; return pat.withQuerySpanMaybe(qf).withHap(ef).splitQueries(); @@ -3361,7 +3361,7 @@ export const shrinklist = (amount, pat) => pat.shrinklist(amount); Pattern.prototype.growlist = function (amount) { return this.shrinklist(amount).reverse(); -} +}; export const growlist = (amount, pat) => pat.growlist(amount); /** From def17382599fd998ce8cf25c1b66f22bb8ec6f8b Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 19 Jun 2026 21:29:25 +0100 Subject: [PATCH 3/5] shortcuts for workshop --- packages/mqtt/mqtt.mjs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs index 96659c67b..a84628567 100644 --- a/packages/mqtt/mqtt.mjs +++ b/packages/mqtt/mqtt.mjs @@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { Pattern, isPattern } from '@strudel/core'; +import { Pattern, isPattern, createParams } from '@strudel/core'; import Paho from 'paho-mqtt'; const connections = {}; @@ -118,3 +118,11 @@ Pattern.prototype.mqtt = function ( return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true }); }); }; + + +// This adds the 'move' and 'motor' commands to strudel +export const { move, motor } = createParams('move', 'motor'); window.move = move; window.motor = motor; +// This adds the 'robot' command +Pattern.prototype.robot = function (robot_id, address = 'ws://192.168.8.248:9001/mqtt') { + return this.mqtt(undefined, undefined, '/move/' + robot_id, address); +}; From d3e2b7c7b4740636032f500547bf36221f9e8bd3 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 19 Jun 2026 21:29:49 +0100 Subject: [PATCH 4/5] motors workshop page --- website/src/pages/workshop/motors.mdx | 119 ++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 website/src/pages/workshop/motors.mdx diff --git a/website/src/pages/workshop/motors.mdx b/website/src/pages/workshop/motors.mdx new file mode 100644 index 000000000..9d679dee9 --- /dev/null +++ b/website/src/pages/workshop/motors.mdx @@ -0,0 +1,119 @@ +--- +title: Movement with Strudel +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '@src/docs/MiniRepl'; +import Box from '@components/Box.astro'; +import QA from '@components/QA'; + +# Controlling motors with Strudel + +Strudel is mainly made for making music, but it's possible to pattern other things with it, including motors. + +We're going to use a microcontroller for this, called an "[Inventor 2040W](https://shop.pimoroni.com/products/inventor-2040-w)", which is +a [Pico W](https://shop.pimoroni.com/products/inventor-2040-w?variant=40053063155795) with extra ports added including some for controlling motors. + +![image](https://shop.pimoroni.com/cdn/shop/products/Inventor2040_1of3_1500x1500_crop_center.jpg?v=1656927927) + +## Technical details + +Feel free to gloss over these! + +- The Inventor 2040W connects to the internet wirelessly, and it can power from a battery or USB. Hopefully the batteries last! +- It's running [some code](https://github.com/patternclub/alpacalab/blob/main/course/main.py) that listens for messages using an "Internet of Things" network protocol (called MQTT). When it receives a message, it moves a motor. +- It connects to a small server (running software called 'mosquitto') on Alex's laptop. +- Strudel can send these messages instead of triggering sounds - that's how we use it to pattern movement. + +## First movement + +Let's get a motor running! + +1. Note the letter drawn on a label on the back of the microcontroller. + +2. Plug a battery into your microcontroller. + +3. Plug a motor into 'servo' (not motor) plug numbered 1, with the yellow (lightest) cable closest to the '1', and the brown (darkest) cable outward + +4. Run the below to set up some values, changing the `x` in 'robot('x')` to the letter on your microcontroller. + + + + + +If you refresh the page, you'll need to change the letter to match your robot again. + +If your motor starts moving unexpectedly, someone else might have put your letter in by mistake! + +Note that in the above, we start counting motors from '0', so motor 1 on the board is motor 0 in the code. + + + +## Patterning movement + +Many strudel features for playing with sound patterns will work when +playing with motor patterns. Try playing with the mininotation in the +`move` command: + + + +The move instructions are in the range from -90 to 90. + + + If your motors stop working at some point, and your code looks right, try pressing the 'reset' button on the + microcontroller. + + +It's possible to make smooth movements based on different 'waveforms', for example a smooth sinewave: + + + +The movement is still quite jerky, because the 'segment' command is +only taking 16 positions from the sinewave. Try increasing it to 32 or 64. It's best not too much higher than that, as the microcontroller +might get overwhelmed with a backlog of instructions! + + + Try replacing `sine` with other waveforms: `saw` (sawtooth wave), `tri` (triangular wave) are good, and there is also + `rand` (random wave) and `perlin` (a kind of smoothed-out randomness). + + +## Patterning more than one motor + +You can pattern the `motor` command separately from the `move` one: + + + +Alternatively, you can pattern two motors in separate patterns. The below sends the same pattern for the first two motors, but with the second one running slower: + + + + From 85e6d436ef6409f7708409d98135e65575cf0936 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 19 Jun 2026 21:29:58 +0100 Subject: [PATCH 5/5] motors workshop page --- website/src/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/website/src/config.ts b/website/src/config.ts index eb098d009..12272dbcb 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -68,6 +68,7 @@ export const SIDEBAR: Sidebar = { { text: 'Pattern Effects', link: 'workshop/pattern-effects' }, { text: 'Recap', link: 'workshop/recap' }, { text: 'Workshop in German', link: 'de/workshop/getting-started' }, + { text: 'Tanglebot workshop', link: 'workshop/motors' }, ], 'Making Sound': [ { text: 'Samples', link: 'learn/samples' },