mirror of
https://codeberg.org/uzu/strudel
synced 2026-08-08 15:42:56 -04:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8722c94bbe | |||
| b606bf4692 | |||
| c150752372 | |||
| e248bf85f3 | |||
| 6c51c0261a | |||
| deefde7b50 | |||
| d4f63e8de3 | |||
| 19cb3dedc2 | |||
| a7c3407da7 | |||
| 6870b04fb2 | |||
| 2b2646a768 | |||
| 97ef5bc335 | |||
| 85e6d436ef | |||
| d3e2b7c7b4 | |||
| def1738259 | |||
| 0feeb1e701 | |||
| 813cc6c190 | |||
| 62d4f84e69 | |||
| 49a1e11cd8 | |||
| 475f17ddfd | |||
| bd68c6a0a7 | |||
| f21aeb55bd | |||
| d83139980b |
@@ -1457,6 +1457,27 @@ export function stack(...pats) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The given items are played at the same time at the same length.
|
||||||
|
*
|
||||||
|
* @tags temporal
|
||||||
|
* @return {Pattern}
|
||||||
|
* @synonyms polyrhythm, pr
|
||||||
|
* @example
|
||||||
|
* mute_stack("g3", "b3", ["e4", "d4"]).note()
|
||||||
|
* // "g3,b3,[e4 d4]".note()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // As a chained function:
|
||||||
|
* s("hh*4").mute_stack(
|
||||||
|
* note("c4(5,8)")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
export function mute_stack(...pats) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
|
|
||||||
function _stackWith(func, pats) {
|
function _stackWith(func, pats) {
|
||||||
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
|
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
|
||||||
if (pats.length === 0) {
|
if (pats.length === 0) {
|
||||||
@@ -3359,6 +3380,11 @@ Pattern.prototype.shrinklist = function (amount) {
|
|||||||
|
|
||||||
export const shrinklist = (amount, pat) => pat.shrinklist(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*
|
* *Experimental*
|
||||||
*
|
*
|
||||||
@@ -4127,3 +4153,59 @@ Pattern.prototype.worklet = function (src, ...inputs) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const worklet = (...args) => pure({}).worklet(...args);
|
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();
|
||||||
|
};
|
||||||
|
|||||||
+55
-13
@@ -5,6 +5,12 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// evolved from https://garten.salat.dev/lisp/parser.html
|
// evolved from https://garten.salat.dev/lisp/parser.html
|
||||||
|
|
||||||
|
let recurse = 0
|
||||||
|
function lrec(...args) {
|
||||||
|
recurse += 1
|
||||||
|
console.info(recurse, ...args)
|
||||||
|
}
|
||||||
export class MondoParser {
|
export class MondoParser {
|
||||||
// these are the tokens we expect
|
// these are the tokens we expect
|
||||||
token_types = {
|
token_types = {
|
||||||
@@ -24,10 +30,17 @@ export class MondoParser {
|
|||||||
op: /^[*/:!@%?+\-&]|^\.{2}/, // * / : ! @ % ? ..
|
op: /^[*/:!@%?+\-&]|^\.{2}/, // * / : ! @ % ? ..
|
||||||
// dollar: /^\$/,
|
// dollar: /^\$/,
|
||||||
pipe: /^#/,
|
pipe: /^#/,
|
||||||
stack: /^[,$]/,
|
// Matches _$ or _$BASS
|
||||||
|
mute_stack: /^_\$([a-zA-Z0-9_]+)?/,
|
||||||
|
// Matches S$ or S$VOCALS
|
||||||
|
solo_stack: /^S\$([a-zA-Z0-9_]+)?/,
|
||||||
|
// stack: /^[,$]/,
|
||||||
|
stack: /^,|^\$([a-zA-Z0-9_]+)?/,
|
||||||
or: /^[|]/,
|
or: /^[|]/,
|
||||||
plain: /^[a-zA-Z0-9-~_^#]+/,
|
plain: /^[a-zA-Z0-9-~_^#]+/,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
solo_enabled = false;
|
||||||
op_precedence = [['*', '/', ':', '!', '@', '%', '?', '+', '-', '..'], ['&']];
|
op_precedence = [['*', '/', ':', '!', '@', '%', '?', '+', '-', '..'], ['&']];
|
||||||
// matches next token
|
// matches next token
|
||||||
next_token(code, offset = 0) {
|
next_token(code, offset = 0) {
|
||||||
@@ -62,6 +75,7 @@ export class MondoParser {
|
|||||||
offset += token.value.length;
|
offset += token.value.length;
|
||||||
tokens.push(token);
|
tokens.push(token);
|
||||||
}
|
}
|
||||||
|
lrec("TOKENS", tokens[0], tokens[1])
|
||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
// take code, return abstract syntax tree
|
// take code, return abstract syntax tree
|
||||||
@@ -73,19 +87,22 @@ export class MondoParser {
|
|||||||
while (this.tokens.length) {
|
while (this.tokens.length) {
|
||||||
expressions.push(this.parse_expr());
|
expressions.push(this.parse_expr());
|
||||||
}
|
}
|
||||||
|
let parsed = expressions[0]
|
||||||
if (expressions.length === 0) {
|
if (expressions.length === 0) {
|
||||||
// empty case
|
// empty case
|
||||||
return { type: 'list', children: [] };
|
parsed = { type: 'list', children: [] };
|
||||||
}
|
} else if (expressions.length > 1 || expressions[0].type !== 'list')
|
||||||
// do we have multiple top level expressions or a single non list?
|
// do we have multiple top level expressions or a single non list?
|
||||||
if (expressions.length > 1 || expressions[0].type !== 'list') {
|
{
|
||||||
return {
|
parsed = {
|
||||||
type: 'list',
|
type: 'list',
|
||||||
children: this.desugar(expressions),
|
children: this.desugar(expressions),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lrec("PARSED", parsed)
|
||||||
// we have a single list
|
// we have a single list
|
||||||
return expressions[0];
|
return parsed;
|
||||||
}
|
}
|
||||||
// parses any valid expression
|
// parses any valid expression
|
||||||
parse_expr() {
|
parse_expr() {
|
||||||
@@ -120,8 +137,11 @@ export class MondoParser {
|
|||||||
children = children.slice(splitIndex + 1);
|
children = children.slice(splitIndex + 1);
|
||||||
}
|
}
|
||||||
chunks.push(children);
|
chunks.push(children);
|
||||||
|
lrec("chunks", chunks)
|
||||||
return chunks;
|
return chunks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
desugar_split(children, split_type, next) {
|
desugar_split(children, split_type, next) {
|
||||||
const chunks = this.split_children(children, split_type);
|
const chunks = this.split_children(children, split_type);
|
||||||
if (chunks.length === 1) {
|
if (chunks.length === 1) {
|
||||||
@@ -253,12 +273,15 @@ export class MondoParser {
|
|||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
desugar(children, type) {
|
desugar(children, type) {
|
||||||
// if type is given, the first element is expected to contain it as plain value
|
// if type is given, the first element is expected to contain it as plain value
|
||||||
// e.g. with (square a b, c), we want to split (a b, c) and ignore "square"
|
// e.g. with (square a b, c), we want to split (a b, c) and ignore "square"
|
||||||
children = type ? children.slice(1) : children;
|
children = type ? children.slice(1) : children;
|
||||||
children = this.desugar_split(children, 'stack', (children) =>
|
|
||||||
this.desugar_split(children, 'or', (children) => {
|
const desugar_split_children = (children) => {
|
||||||
|
return this.desugar_split(children, 'or', (children) => {
|
||||||
|
console.info("TYPE", type)
|
||||||
// chunks of multiple args
|
// chunks of multiple args
|
||||||
if (type) {
|
if (type) {
|
||||||
// the type we've removed before splitting needs to be added back
|
// the type we've removed before splitting needs to be added back
|
||||||
@@ -269,14 +292,34 @@ export class MondoParser {
|
|||||||
children = this.desugar_ops(children, ops);
|
children = this.desugar_ops(children, ops);
|
||||||
});
|
});
|
||||||
children = this.desugar_pipes(children);
|
children = this.desugar_pipes(children);
|
||||||
|
lrec("STACK CHILDREN", children)
|
||||||
return children;
|
return children;
|
||||||
}),
|
})
|
||||||
);
|
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info("PRE_CHILDREN", children)
|
||||||
|
// children = this.desugar_split(children, 'mute_stack', (children) => {
|
||||||
|
// const x = desugar_split_children(children)
|
||||||
|
// // lrec({x})
|
||||||
|
// return x
|
||||||
|
// })
|
||||||
|
|
||||||
|
lrec({ children })
|
||||||
|
|
||||||
|
|
||||||
|
children = this.desugar_split(children, 'stack', (children) => {
|
||||||
|
return desugar_split_children(children)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
lrec("CHILDREN", children)
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
parse_list() {
|
parse_list() {
|
||||||
let node = this.parse_pair('open_list', 'close_list');
|
let node = this.parse_pair('open_list', 'close_list');
|
||||||
node.children = this.desugar(node.children);
|
node.children = this.desugar(node.children);
|
||||||
|
lrec("node", node)
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
parse_angle() {
|
parse_angle() {
|
||||||
@@ -325,9 +368,8 @@ export function printAst(ast, compact = false, lvl = 0) {
|
|||||||
const br = compact ? '' : '\n';
|
const br = compact ? '' : '\n';
|
||||||
const spaces = compact ? '' : Array(lvl).fill(' ').join('');
|
const spaces = compact ? '' : Array(lvl).fill(' ').join('');
|
||||||
if (ast.type === 'list') {
|
if (ast.type === 'list') {
|
||||||
return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${
|
return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')'
|
||||||
ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')'
|
}`;
|
||||||
}`;
|
|
||||||
}
|
}
|
||||||
return `${ast.value}`;
|
return `${ast.value}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,10 +49,11 @@ lib['or'] = (...children) => chooseIn(...children); // always has structure but
|
|||||||
//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct
|
//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct
|
||||||
|
|
||||||
function evaluator(node, scope) {
|
function evaluator(node, scope) {
|
||||||
const { type } = node;
|
const { type,children } = node;
|
||||||
// node is list
|
// node is list]
|
||||||
if (type === 'list') {
|
if (type === 'list' && children.length) {
|
||||||
const { children } = node;
|
// const { children } = node;
|
||||||
|
|
||||||
const [name, ...args] = children;
|
const [name, ...args] = children;
|
||||||
// some functions wont be reified to make sure they work (e.g. see extend below)
|
// some functions wont be reified to make sure they work (e.g. see extend below)
|
||||||
if (typeof name === 'function') {
|
if (typeof name === 'function') {
|
||||||
@@ -65,6 +66,7 @@ function evaluator(node, scope) {
|
|||||||
const first = name.firstCycle(true)[0];
|
const first = name.firstCycle(true)[0];
|
||||||
const type = typeof first?.value;
|
const type = typeof first?.value;
|
||||||
if (type !== 'function') {
|
if (type !== 'function') {
|
||||||
|
console.error("first", first)
|
||||||
throw new Error(`[mondough] expected function, got "${first?.value}"`);
|
throw new Error(`[mondough] expected function, got "${first?.value}"`);
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
@@ -76,12 +78,16 @@ function evaluator(node, scope) {
|
|||||||
})
|
})
|
||||||
.innerJoin();
|
.innerJoin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.info("NODE", node)
|
||||||
// node is leaf
|
// node is leaf
|
||||||
let { value } = node;
|
let { value } = node;
|
||||||
if (type === 'plain' && scope[value]) {
|
if (type === 'plain' && scope[value]) {
|
||||||
return reify(scope[value]); // -> local scope has no location
|
return reify(scope[value]); // -> local scope has no location
|
||||||
}
|
}
|
||||||
const variable = lib[value] ?? strudelScope[value];
|
const variable = lib[value] ?? strudelScope[value];
|
||||||
|
|
||||||
|
console.info("VARIABLE", variable)
|
||||||
// problem: collisions when we want a string that happens to also be a variable name
|
// problem: collisions when we want a string that happens to also be a variable name
|
||||||
// example: "s sine" -> sine is also a variable
|
// example: "s sine" -> sine is also a variable
|
||||||
let pat;
|
let pat;
|
||||||
@@ -107,6 +113,7 @@ export function mondo(code, offset = 0) {
|
|||||||
code = code.join('');
|
code = code.join('');
|
||||||
}
|
}
|
||||||
const pat = runner.run(code, undefined, offset);
|
const pat = runner.run(code, undefined, offset);
|
||||||
|
console.info("MONDO_PAT", pat)
|
||||||
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
|
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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/>.
|
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';
|
import Paho from 'paho-mqtt';
|
||||||
|
|
||||||
const connections = {};
|
const connections = {};
|
||||||
@@ -118,3 +118,12 @@ Pattern.prototype.mqtt = function (
|
|||||||
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
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`] = `
|
exports[`runs examples > example "beat" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/16 | s:bd ]",
|
"[ 0/1 → 1/16 | s:bd ]",
|
||||||
|
|||||||
@@ -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