mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-24 22:55:38 -04:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c51c0261a | |||
| deefde7b50 | |||
| d4f63e8de3 | |||
| 19cb3dedc2 | |||
| a7c3407da7 | |||
| 6870b04fb2 | |||
| 2b2646a768 | |||
| 97ef5bc335 | |||
| 85e6d436ef | |||
| d3e2b7c7b4 | |||
| def1738259 | |||
| 0feeb1e701 | |||
| 813cc6c190 | |||
| 62d4f84e69 | |||
| 49a1e11cd8 | |||
| 941c97da0d | |||
| 95a9d301a1 | |||
| 475f17ddfd | |||
| bd68c6a0a7 | |||
| 2635716c87 | |||
| 58f956b57e | |||
| 0ae2c120b3 | |||
| 05a43ef687 | |||
| f21aeb55bd | |||
| d83139980b | |||
| 0c61cd7670 |
@@ -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*
|
||||
*
|
||||
@@ -4127,3 +4132,59 @@ Pattern.prototype.worklet = function (src, ...inputs) {
|
||||
};
|
||||
|
||||
export const worklet = (...args) => pure({}).worklet(...args);
|
||||
|
||||
/**
|
||||
* Creates a pattern of numbers in base b from a number or pattern of numbers
|
||||
* limited to d digits long from the right
|
||||
*
|
||||
* @name base
|
||||
* @tags generators
|
||||
* @param {number} n - number to convert (can be a pattern or array)
|
||||
* @param {number} b - base to convert to (defaults to 10) (can be a pattern)
|
||||
* @param {number} d - max number of digits to produce for each n (defaults to 0 for all) (can be a pattern)
|
||||
* @example
|
||||
* $: note(base("7175 543", 10, 3)).scale("c:major").s("saw")
|
||||
* // $: note("1 7 5 5 4 3").scale("c:major").s("saw")
|
||||
*/
|
||||
export const base = (n, b = 10, d = 0) => {
|
||||
if (Array.isArray(n)) {
|
||||
n = sequence(n);
|
||||
}
|
||||
n = reify(n);
|
||||
b = reify(b);
|
||||
d = reify(d);
|
||||
|
||||
return d
|
||||
.withValue((e) => {
|
||||
return b
|
||||
.withValue((c) => {
|
||||
return n
|
||||
.withValue((v) => {
|
||||
let digits = [];
|
||||
let value = v;
|
||||
while (value > 0) {
|
||||
digits.unshift(value % c);
|
||||
value = Math.floor(value / c);
|
||||
}
|
||||
if (e) {
|
||||
const l = digits.length;
|
||||
if (l > e) {
|
||||
digits = digits.slice(-1 * e);
|
||||
}
|
||||
/*
|
||||
if (l < e){
|
||||
for (let i = l; i < e; i++) {
|
||||
digits.unshift("~");//0); //Would like to be padding this but ~- doesn't work
|
||||
}
|
||||
console.log("digits", digits);
|
||||
}
|
||||
*/
|
||||
}
|
||||
return sequence(digits);
|
||||
})
|
||||
.squeezeJoin();
|
||||
})
|
||||
.squeezeJoin();
|
||||
})
|
||||
.squeezeJoin();
|
||||
};
|
||||
|
||||
@@ -537,6 +537,9 @@ export async function midin(input) {
|
||||
* The note length is fixed as Superdough is not currently set up for undetermined
|
||||
* note durations
|
||||
*
|
||||
* The 'midichan' control value contains the number of the channel the note is coming from
|
||||
* so it could be filtered or manipulated further in the chain.
|
||||
*
|
||||
* @name midikeys
|
||||
* @tags external_io, midi
|
||||
* @param {string | number} input MIDI device name or index defaulting to 0
|
||||
@@ -552,6 +555,10 @@ export async function midin(input) {
|
||||
* .s("saw")
|
||||
* .add(note(rand.mul(0.3)))
|
||||
* .lpf(1000).lpe(2).room(0.5)
|
||||
* @example
|
||||
* // discard all notes not coming out from midi channel 2
|
||||
* const kb = await midikeys('Arturia KeyStep 32')
|
||||
* kb().filterValues(v=>v.midichan==2).s("tri")
|
||||
*/
|
||||
const kHaps = {};
|
||||
const kListeners = {};
|
||||
@@ -633,7 +640,7 @@ export async function midikeys(input) {
|
||||
*/
|
||||
return;
|
||||
} else {
|
||||
value = { ...value, note: Math.round(note), velocity: velocity / 127 };
|
||||
value = { ...value, note: Math.round(note), velocity: velocity / 127, midichan: message.channel };
|
||||
}
|
||||
kHaps[input].push(new Hap(span, span, value, {}));
|
||||
if (!noteoff && triggerAvailable) {
|
||||
|
||||
+10
-1
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Pattern, isPattern } from '@strudel/core';
|
||||
import { Pattern, isPattern, createParams } from '@strudel/core';
|
||||
import Paho from 'paho-mqtt';
|
||||
|
||||
const connections = {};
|
||||
@@ -118,3 +118,12 @@ 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);
|
||||
};
|
||||
|
||||
@@ -1242,6 +1242,35 @@ exports[`runs examples > example "bank" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "base" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:D3 s:saw ]",
|
||||
"[ 1/6 → 1/3 | note:C4 s:saw ]",
|
||||
"[ 1/3 → 1/2 | note:A3 s:saw ]",
|
||||
"[ 1/2 → 2/3 | note:A3 s:saw ]",
|
||||
"[ 2/3 → 5/6 | note:G3 s:saw ]",
|
||||
"[ 5/6 → 1/1 | note:F3 s:saw ]",
|
||||
"[ 1/1 → 7/6 | note:D3 s:saw ]",
|
||||
"[ 7/6 → 4/3 | note:C4 s:saw ]",
|
||||
"[ 4/3 → 3/2 | note:A3 s:saw ]",
|
||||
"[ 3/2 → 5/3 | note:A3 s:saw ]",
|
||||
"[ 5/3 → 11/6 | note:G3 s:saw ]",
|
||||
"[ 11/6 → 2/1 | note:F3 s:saw ]",
|
||||
"[ 2/1 → 13/6 | note:D3 s:saw ]",
|
||||
"[ 13/6 → 7/3 | note:C4 s:saw ]",
|
||||
"[ 7/3 → 5/2 | note:A3 s:saw ]",
|
||||
"[ 5/2 → 8/3 | note:A3 s:saw ]",
|
||||
"[ 8/3 → 17/6 | note:G3 s:saw ]",
|
||||
"[ 17/6 → 3/1 | note:F3 s:saw ]",
|
||||
"[ 3/1 → 19/6 | note:D3 s:saw ]",
|
||||
"[ 19/6 → 10/3 | note:C4 s:saw ]",
|
||||
"[ 10/3 → 7/2 | note:A3 s:saw ]",
|
||||
"[ 7/2 → 11/3 | note:A3 s:saw ]",
|
||||
"[ 11/3 → 23/6 | note:G3 s:saw ]",
|
||||
"[ 23/6 → 4/1 | note:F3 s:saw ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "beat" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/16 | s:bd ]",
|
||||
@@ -7927,6 +7956,8 @@ exports[`runs examples > example "midikeys" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "midikeys" example index 1 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "midikeys" example index 2 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "midin" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c cutoff:0 resonance:0 s:sawtooth ]",
|
||||
|
||||
@@ -78,6 +78,7 @@ export default defineConfig({
|
||||
urlPattern: ({ url }) =>
|
||||
[
|
||||
/^https:\/\/raw\.githubusercontent\.com\/.*/i,
|
||||
/^https:\/\/strudel\.b-cdn\.net\/.*/i,
|
||||
/^https:\/\/freesound\.org\/.*/i,
|
||||
/^https:\/\/cdn\.freesound\.org\/.*/i,
|
||||
/^https:\/\/shabda\.ndre\.gr\/.*/i,
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: move("-60 80").motor("0").robot('x');
|
||||
`}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
|
||||
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.
|
||||
|
||||
</Box>
|
||||
|
||||
## 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:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: move("-10 0 10 [20 30]*2").motor("0").slow(2).robot('x')
|
||||
`}
|
||||
/>
|
||||
|
||||
The move instructions are in the range from -90 to 90.
|
||||
|
||||
<box>
|
||||
If your motors stop working at some point, and your code looks right, try pressing the 'reset' button on the
|
||||
microcontroller.
|
||||
</box>
|
||||
|
||||
It's possible to make smooth movements based on different 'waveforms', for example a smooth sinewave:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: move(sine.range(-30, 30).segment(16)).motor("0").slow(2).robot('x');
|
||||
`}
|
||||
/>
|
||||
|
||||
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!
|
||||
|
||||
<box>
|
||||
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).
|
||||
</box>
|
||||
|
||||
## Patterning more than one motor
|
||||
|
||||
You can pattern the `motor` command separately from the `move` one:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: move("-10 0 10 [20 30]*2").motor("0 1").slow(2).robot('x');
|
||||
`}
|
||||
/>
|
||||
|
||||
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:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
|
||||
$: move("-10 0 10 [20 30]\*2").motor("0").slow(2).robot('x');
|
||||
|
||||
$: move("-10 0 10 [20 30]\*2").motor("1").slow(3).robot('x');
|
||||
|
||||
`}
|
||||
/>
|
||||
Reference in New Issue
Block a user