Compare commits

..

16 Commits

Author SHA1 Message Date
Alex McLean 70c932a760 Merge branch 'cycler' into textbox 2025-10-14 12:39:26 +02:00
Alex McLean f5631a01ae fixes 2025-10-14 11:02:53 +01:00
Alex McLean 989b5dcc3a thingie to display current pattern value 2025-10-14 10:41:57 +01:00
alex 5de891f31d format 2025-10-14 08:57:25 +01:00
alex 325e348bbf Merge branch 'main' into cycler 2025-10-14 08:56:47 +01:00
alex fe117e23fd start of tap tempo 2025-10-14 08:56:11 +01:00
Alex McLean 69a82ddc5b low latency clock control experimentation 2025-10-08 12:18:32 +01:00
Alex McLean 4209506fd0 Merge branch 'main' into cycler 2025-10-01 13:27:49 +02:00
Alex McLean ff790c803d format 2025-10-01 12:01:19 +01:00
Alex McLean f0efaabe20 Merge branch 'main' into cycler 2025-10-01 11:58:19 +01:00
alex 5097ae8fc0 use encoder disc 2025-03-01 18:07:25 +00:00
alex 6b1e6cc7af try to make more reactish 2025-03-01 16:53:13 +00:00
alex 9c6eca9a59 it works 2025-03-01 15:55:10 +00:00
alex 02bb88bde4 rm 2025-03-01 10:57:39 +00:00
alex c16511dfbd towards spinning a disk matching cycles inside strudel 2025-03-01 10:55:37 +00:00
alex 5054756ca2 experiment 2025-02-28 17:20:28 +00:00
57 changed files with 1124 additions and 1481 deletions
-37
View File
@@ -1,37 +0,0 @@
name: Build and Deploy to beta (warm.strudel.cc)
on: [workflow_dispatch]
# Allow one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
build:
runs-on: docker
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9.12.2
- uses: actions/setup-node@v4
with:
node-version: 20
# cache: "pnpm"
- name: Install Dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Deploy
run: |
eval $(ssh-agent -s)
echo "$SSH_PRIVATE_KEY" | ssh-add -
apt update && apt install -y rsync
mkdir ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/warm.strudel.cc
+2 -2
View File
@@ -1,4 +1,4 @@
name: Build and Deploy to live (strudel.cc)
name: Build and Deploy
on: [workflow_dispatch]
@@ -34,4 +34,4 @@ jobs:
apt update && apt install -y rsync
mkdir ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy
-14
View File
@@ -45,20 +45,6 @@ tidal-drum-machines
webaudiofontdata
src-tauri/target
### BEGIN Visual Studio Code ###
# Blanket, recursive exclude for .vscode directory and files
.vscode/**/*
# Unexclude specific files and directories within .vscode
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### END Visual Studio Code ###
# BEGIN JetBrains -> END JetBrains
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+12 -340
View File
@@ -1,9 +1,6 @@
import jsdoc from '../../doc.json';
import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html';
import { Scale } from '@tonaljs/tonal';
import { soundMap } from 'superdough';
import { complex } from '@strudel/tonal';
const escapeHtml = (str) => {
const div = document.createElement('div');
@@ -78,51 +75,6 @@ const isValidDoc = (doc) => {
const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
export function bankCompletions() {
const soundDict = soundMap.get();
const banks = new Set();
for (const key of Object.keys(soundDict)) {
const [bank, suffix] = key.split('_');
if (suffix && bank) banks.add(bank);
}
return Array.from(banks)
.sort()
.map((name) => ({ label: name, type: 'bank' }));
}
// Attempt to get all scale names from Tonal
let scaleCompletions = [];
try {
scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
} catch (e) {
console.warn('[autocomplete] Could not load scale names from Tonal:', e);
}
// Valid mode values for voicing
const modeCompletions = [
{ label: 'below', type: 'mode' },
{ label: 'above', type: 'mode' },
{ label: 'duck', type: 'mode' },
{ label: 'root', type: 'mode' },
];
// Valid chord symbols from ireal dictionary plus empty string for major triads
const chordSymbols = ['', ...Object.keys(complex)].sort();
const chordSymbolCompletions = chordSymbols.map((symbol) => {
if (symbol === '') {
return {
label: 'major',
apply: '',
type: 'chord-symbol',
};
}
return {
label: symbol,
apply: symbol,
type: 'chord-symbol',
};
});
export const getSynonymDoc = (doc, synonym) => {
const synonyms = doc.synonyms || [];
const docLabel = getDocLabel(doc);
@@ -161,299 +113,19 @@ const jsdocCompletions = (() => {
return completions;
})();
// --- Handler functions for each context ---
const pitchNames = [
'C',
'C#',
'Db',
'D',
'D#',
'Eb',
'E',
'E#',
'Fb',
'F',
'F#',
'Gb',
'G',
'G#',
'Ab',
'A',
'A#',
'Bb',
'B',
'B#',
'Cb',
];
// Cached regex patterns for scaleHandler
const SCALE_NO_QUOTES_REGEX = /scale\(\s*$/;
const SCALE_AFTER_COLON_REGEX = /scale\(\s*['"][^'"]*:[^'"]*$/;
const SCALE_PRE_COLON_REGEX = /scale\(\s*['"][^'"]*$/;
const SCALE_PITCH_MATCH_REGEX = /([A-Ga-g][#b]*)?$/;
const SCALE_SPACES_TO_COLON_REGEX = /\s+/g;
function scaleHandler(context) {
// First check for scale context without quotes - block with empty completions
let scaleNoQuotesContext = context.matchBefore(SCALE_NO_QUOTES_REGEX);
if (scaleNoQuotesContext) {
return {
from: scaleNoQuotesContext.to,
options: [],
};
}
// Check for after-colon context first (more specific)
let scaleAfterColonContext = context.matchBefore(SCALE_AFTER_COLON_REGEX);
if (scaleAfterColonContext) {
const text = scaleAfterColonContext.text;
const colonIdx = text.lastIndexOf(':');
if (colonIdx !== -1) {
const fragment = text.slice(colonIdx + 1);
const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
const options = filteredScales.map((s) => ({
...s,
apply: s.label.replace(SCALE_SPACES_TO_COLON_REGEX, ':'),
}));
const from = scaleAfterColonContext.from + colonIdx + 1;
return {
from,
options,
};
}
}
// Then check for pre-colon context
let scalePreColonContext = context.matchBefore(SCALE_PRE_COLON_REGEX);
if (scalePreColonContext) {
if (!scalePreColonContext.text.includes(':')) {
if (context.explicit) {
const text = scalePreColonContext.text;
const match = text.match(SCALE_PITCH_MATCH_REGEX);
const fragment = match ? match[0] : '';
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const from = scalePreColonContext.to - fragment.length;
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
return { from, options };
} else {
return { from: scalePreColonContext.to, options: [] };
}
}
}
return null;
}
// Cached regex patterns for soundHandler
const SOUND_NO_QUOTES_REGEX = /(s|sound)\(\s*$/;
const SOUND_WITH_QUOTES_REGEX = /(s|sound)\(\s*['"][^'"]*$/;
const SOUND_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w]*)$/;
function soundHandler(context) {
// First check for sound context without quotes - block with empty completions
let soundNoQuotesContext = context.matchBefore(SOUND_NO_QUOTES_REGEX);
if (soundNoQuotesContext) {
return {
from: soundNoQuotesContext.to,
options: [],
};
}
// Then check for sound context with quotes - provide completions
let soundContext = context.matchBefore(SOUND_WITH_QUOTES_REGEX);
if (!soundContext) return null;
const text = soundContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
const soundNames = Object.keys(soundMap.get()).sort();
const filteredSounds = soundNames.filter((name) => name.includes(fragment));
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
const from = soundContext.to - fragment.length;
return {
from,
options,
};
}
// Cached regex patterns for bankHandler
const BANK_NO_QUOTES_REGEX = /bank\(\s*$/;
const BANK_WITH_QUOTES_REGEX = /bank\(\s*['"][^'"]*$/;
function bankHandler(context) {
// First check for bank context without quotes - block with empty completions
let bankNoQuotesContext = context.matchBefore(BANK_NO_QUOTES_REGEX);
if (bankNoQuotesContext) {
return {
from: bankNoQuotesContext.to,
options: [],
};
}
// Then check for bank context with quotes - provide completions
let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX);
if (!bankMatch) return null;
const text = bankMatch.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragment = inside;
let banks = bankCompletions();
const filteredBanks = banks.filter((b) => b.label.startsWith(fragment));
const from = bankMatch.to - fragment.length;
return {
from,
options: filteredBanks,
};
}
// Cached regex patterns for modeHandler
const MODE_NO_QUOTES_REGEX = /mode\(\s*$/;
const MODE_AFTER_COLON_REGEX = /mode\(\s*['"][^'"]*:[^'"]*$/;
const MODE_PRE_COLON_REGEX = /mode\(\s*['"][^'"]*$/;
const MODE_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w:]*)$/;
function modeHandler(context) {
// First check for mode context without quotes - block with empty completions
let modeNoQuotesContext = context.matchBefore(MODE_NO_QUOTES_REGEX);
if (modeNoQuotesContext) {
return {
from: modeNoQuotesContext.to,
options: [],
};
}
// Check for after-colon context first (more specific)
let modeAfterColonContext = context.matchBefore(MODE_AFTER_COLON_REGEX);
if (modeAfterColonContext) {
const text = modeAfterColonContext.text;
const colonIdx = text.lastIndexOf(':');
if (colonIdx !== -1) {
const fragment = text.slice(colonIdx + 1);
// For anchor after colon, we can suggest pitch names
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
const from = modeAfterColonContext.from + colonIdx + 1;
return {
from,
options,
};
}
}
// Then check for pre-colon context
let modeContext = context.matchBefore(MODE_PRE_COLON_REGEX);
if (!modeContext) return null;
const text = modeContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
const fragMatch = inside.match(MODE_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment));
const from = modeContext.to - fragment.length;
return {
from,
options: filteredModes,
};
}
// Cached regex patterns for chordHandler
const CHORD_NO_QUOTES_REGEX = /chord\(\s*$/;
const CHORD_WITH_QUOTES_REGEX = /chord\(\s*['"][^'"]*$/;
const CHORD_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w#b+^:-]*)$/;
function chordHandler(context) {
// First check for chord context without quotes - block with empty completions
let chordNoQuotesContext = context.matchBefore(CHORD_NO_QUOTES_REGEX);
if (chordNoQuotesContext) {
return {
from: chordNoQuotesContext.to,
options: [],
};
}
// Then check for chord context with quotes - provide completions
let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX);
if (!chordContext) return null;
const text = chordContext.text;
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
if (quoteIdx === -1) return null;
const inside = text.slice(quoteIdx + 1);
// Use same fragment matching as sound/mode for expressions like "<G Am>"
const fragMatch = inside.match(CHORD_FRAGMENT_MATCH_REGEX);
const fragment = fragMatch ? fragMatch[1] : inside;
// Check if fragment contains any pitch name at start (for root + symbol)
let rootMatch = null;
let symbolFragment = fragment;
for (const pitch of pitchNames) {
if (fragment.toLowerCase().startsWith(pitch.toLowerCase())) {
rootMatch = pitch;
symbolFragment = fragment.slice(pitch.length);
break;
}
}
if (rootMatch) {
// We have a root, now complete chord symbols
const filteredSymbols = chordSymbolCompletions.filter((s) =>
s.label.toLowerCase().startsWith(symbolFragment.toLowerCase()),
);
// Create completions that replace the entire chord, not just the symbol part
const options = filteredSymbols;
const from = chordContext.to - symbolFragment.length;
return { from, options };
} else {
// No root yet, complete with pitch names
const filteredPitches = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
const options = filteredPitches.map((p) => ({ label: p, type: 'pitch' }));
const from = chordContext.to - fragment.length;
return { from, options };
}
}
// Cached regex patterns for fallbackHandler
const FALLBACK_WORD_REGEX = /\w*/;
function fallbackHandler(context) {
const word = context.matchBefore(FALLBACK_WORD_REGEX);
if (word && word.from === word.to && !context.explicit) return null;
if (word) {
return {
from: word.from,
options: jsdocCompletions,
};
}
return null;
}
const handlers = [
soundHandler,
bankHandler,
chordHandler,
scaleHandler,
modeHandler,
// this handler *must* be last
fallbackHandler,
];
export const strudelAutocomplete = (context) => {
for (const handler of handlers) {
const result = handler(context);
if (result) {
return result;
}
}
return null;
const word = context.matchBefore(/\w*/);
if (word.from === word.to && !context.explicit) return null;
return {
from: word.from,
options: jsdocCompletions,
/* options: [
{ label: 'match', type: 'keyword' },
{ label: 'hello', type: 'variable', info: '(World)' },
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
], */
};
};
export const isAutoCompletionEnabled = (enabled) =>
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.2.6",
"version": "1.2.5",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
@@ -46,11 +46,8 @@
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@tonaljs/tonal": "^4.10.0",
"nanostores": "^0.11.3",
"superdough": "workspace:*"
"nanostores": "^0.11.3"
},
"devDependencies": {
"vite": "^6.0.11"
+6
View File
@@ -140,3 +140,9 @@ registerWidget('_spectrum', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.spectrum({ ...options, ctx, id });
});
registerWidget('_textbox', (id, options = {}, pat) => {
options = { width: 500, height: 30, ...options };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.textbox({ ...options, ctx, id });
});
+15 -3
View File
@@ -12,6 +12,8 @@ function getTime() {
let num_cycles_at_cps_change = 0;
let num_ticks_since_cps_change = 0;
let num_seconds_at_cps_change = 0;
let max_cycle = null;
let cps = 0.5;
// {id: {started: boolean}}
const clients = new Map();
@@ -39,6 +41,7 @@ const sendTick = (phase, duration, tick, time) => {
cps,
time,
cycle,
max_cycle,
});
num_ticks_since_cps_change++;
};
@@ -60,9 +63,10 @@ const stopClock = async (id) => {
const otherClientStarted = Array.from(clients.values()).some((c) => c.started);
//dont stop the clock if other instances are running...
if (!started || otherClientStarted) {
return;
}
// actually do stop it
// if (!started || otherClientStarted) {
// return;
// }
clock.stop();
setCycle(0);
@@ -74,6 +78,10 @@ const setCycle = (cycle) => {
num_cycles_at_cps_change = cycle;
};
const setMaxCycle = (cycle) => {
max_cycle = cycle;
};
const processMessage = (message) => {
const { type, payload } = message;
@@ -92,6 +100,10 @@ const processMessage = (message) => {
setCycle(payload.cycle);
break;
}
case 'setmaxcycle': {
setMaxCycle(payload.maxcycle);
break;
}
case 'toggle': {
if (payload.started) {
startClock(message.id);
-18
View File
@@ -2320,24 +2320,6 @@ export const { miditouch } = registerControl('miditouch');
// TODO: what is this?
export const { polyTouch } = registerControl('polyTouch');
/**
* The host to send open sound control messages to. Requires running the OSC bridge.
* @name oschost
* @param {string | Pattern} oschost e.g. 'localhost'
* @example
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
*/
export const { oschost } = registerControl('oschost');
/**
* The port to send open sound control messages to. Requires running the OSC bridge.
* @name oscport
* @param {number | Pattern} oscport e.g. 57120
* @example
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
*/
export const { oscport } = registerControl('oscport');
export const getControlName = (alias) => {
if (controlAlias.has(alias)) {
return controlAlias.get(alias);
+1 -1
View File
@@ -14,7 +14,7 @@ export class Cyclist {
onToggle,
onError,
getTime,
latency = 0.1,
latency = 0.03,
setInterval,
clearInterval,
beforeStart,
+1 -1
View File
@@ -18,7 +18,7 @@ export function logger(message, type, data = {}) {
}
lastMessage = message;
lastTime = t;
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
console.log(`${t} %c${message}`, 'background-color: black;color:white;border-radius:15px');
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
document.dispatchEvent(
new CustomEvent(logKey, {
+6 -3
View File
@@ -1,6 +1,6 @@
/*
neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances.
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/neocyclist.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
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/>.
*/
@@ -11,6 +11,7 @@ export class NeoCyclist {
constructor({ onTrigger, onToggle, getTime }) {
this.started = false;
this.cps = 0.5;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.getTime = getTime; // get absolute time
this.time_at_last_tick_message = 0;
// the clock of the worker and the audio context clock can drift apart over time
@@ -19,16 +20,18 @@ export class NeoCyclist {
// in order to schedule events consistently.
this.collator = new ClockCollator({ getTargetClockTime: getTime });
this.onToggle = onToggle;
this.latency = 0.1; // fixed trigger time offset
this.latency = -0.1; // fixed trigger time offset
this.cycle = 0;
this.maxcycle = null;
this.id = Math.round(Date.now() * Math.random());
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
this.worker.port.start();
this.channel = new BroadcastChannel('strudeltick');
const tickCallback = (payload) => {
const { cps, begin, end, cycle, time } = payload;
const { cps, begin, end, cycle, maxcycle, time } = payload;
this.cps = cps;
this.cycle = cycle;
this.maxcycle = maxcycle;
const currentTime = this.collator.calculateOffset(time) + time;
processHaps(begin, end, currentTime);
this.time_at_last_tick_message = currentTime;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.5",
"version": "1.2.4",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
-18
View File
@@ -2998,24 +2998,6 @@ export const extend = stepRegister('extend', function (factor, pat) {
return pat.fast(factor).expand(factor);
});
/**
* *Experimental*
*
* `replicate` is similar to `fast` in that it increases its density, but it also increases the step count
* accordingly. So `stepcat("a b".replicate(2), "c d")` would be the same as `"a b a b c d"`, whereas
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
*
* TODO: find out how this function differs from extend
* @example
* stepcat(
* sound("bd bd - cp").replicate(2),
* sound("bd - sd -")
* ).pace(8)
*/
export const replicate = stepRegister('replicate', function (factor, pat) {
return pat.repeatCycles(factor).fast(factor).expand(factor);
});
/**
* *Experimental*
*
+1 -2
View File
@@ -201,8 +201,7 @@ const timeToIntSeed = (x) => xorwise(Math.trunc(_frac(x / 300) * 536870912));
const intSeedToRand = (x) => (x % 536870912) / 536870912;
// Set first random value to be 0.5, otherwise it would be 0. https://github.com/tidalcycles/strudel/issues/1293
const timeToRand = (x) => (x === 0 ? 0.5 : Math.abs(intSeedToRand(timeToIntSeed(x))));
const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
const timeToRandsPrime = (seed, n) => {
const result = [];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.2.6",
"version": "1.2.5",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
-9
View File
@@ -84,18 +84,9 @@ Pattern.prototype.onPaint = function (painter) {
state.controls.painters = [];
}
state.controls.painters.push(painter);
return state;
});
};
// TODO - Why isn't this pure deep copy not working?
// Pattern.prototype.onPaint = function (painter) {
// return this.withState((state) => {
// const painters = state.controls.painters ? [...state.controls.painters, painter] : [painter];
// return new State(state.span, { ...state.controls, painters });
// });
// };
Pattern.prototype.getPainters = function () {
let painters = [];
this.queryArc(0, 0, { painters });
+1
View File
@@ -4,3 +4,4 @@ export * from './draw.mjs';
export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './pitchwheel.mjs';
export * from './textbox.mjs';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.2.5",
"version": "1.2.4",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
+50
View File
@@ -0,0 +1,50 @@
import { getTheme, getDrawContext } from './draw.mjs';
import { Pattern } from '@strudel/core';
export function textbox({ haps, ctx, id, margin = 10, fontsize = 24 } = {}) {
const w = ctx.canvas.width;
const h = ctx.canvas.height;
ctx.clearRect(0, 0, w, h);
const color = getTheme().foreground;
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.globalAlpha = 1;
ctx.textAlign = 'left';
haps.forEach((hap) => {
if (hap.hasOnset()) {
const hapColor = hap.value.color || color;
ctx.strokeStyle = hapColor;
ctx.fillStyle = hapColor;
const { velocity = 1, gain = 1 } = hap.value || {};
const alpha = velocity * gain;
ctx.globalAlpha = alpha;
ctx.font = `${fontsize}px sans-serif`;
ctx.fillText(hap.value, 0, fontsize);
}
});
return;
}
Pattern.prototype.textbox = function (options = {}) {
let { ctx = getDrawContext(), id = 1 } = options;
this.draw(
(haps, time) => {
textbox({
...options,
time,
ctx,
haps: haps.filter((hap) => hap.isActive(time)),
});
},
{
lookbehind: 0,
lookahead: 0,
id,
},
);
return this;
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
"version": "1.2.5",
"version": "1.2.4",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.2.5",
"version": "1.2.4",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.2.6",
"version": "1.2.5",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.2.5",
"version": "1.2.4",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
+2 -2
View File
@@ -5,7 +5,7 @@ import {
slow,
seq,
stepcat,
replicate,
extend,
expand,
pace,
chooseIn,
@@ -36,7 +36,7 @@ lib.square = (...args) => stepcat(...args).setSteps(1);
lib.angle = (...args) => stepcat(...args).pace(1);
lib['*'] = fast;
lib['/'] = slow;
lib['!'] = replicate;
lib['!'] = extend;
lib['@'] = expand;
lib['%'] = pace;
lib['?'] = degradeBy; // todo: default 0.5 not working..
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mondo",
"version": "1.1.5",
"version": "1.1.4",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/motion",
"version": "1.2.5",
"version": "1.2.4",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
"version": "1.2.5",
"version": "1.2.4",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
+16 -17
View File
@@ -4,6 +4,8 @@ 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 OSC from 'osc-js';
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
let connection; // Promise<OSC>
@@ -11,18 +13,19 @@ function connect() {
if (!connection) {
// make sure this runs only once
connection = new Promise((resolve, reject) => {
const ws = new WebSocket('ws://localhost:8080');
ws.addEventListener('open', (event) => {
logger(`[osc] websocket connected`);
resolve(ws);
const osc = new OSC();
osc.open();
osc.on('open', () => {
const url = osc.options?.plugin?.socket?.url;
logger(`[osc] connected${url ? ` to ${url}` : ''}`);
resolve(osc);
});
ws.addEventListener('close', (event) => {
logger(`[osc] websocket closed`);
osc.on('close', () => {
connection = undefined; // allows new connection afterwards
console.log('[osc] disconnected');
reject('OSC connection closed');
});
ws.addEventListener('error', (err) => reject(err));
osc.on('error', (err) => reject(err));
}).catch((err) => {
connection = undefined;
throw new Error('Could not connect to OSC server. Is it running?');
@@ -58,19 +61,15 @@ export function parseControlsFromHap(hap, cps) {
const collator = new ClockCollator({});
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
const ws = await connect();
const osc = await connect();
const controls = parseControlsFromHap(hap, cps);
const keyvals = Object.entries(controls).flat();
const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
if ('oschost' in hap.value) {
msg['host'] = hap.value['oschost'];
}
if ('oscport' in hap.value) {
msg['port'] = hap.value['oscport'];
}
ws.send(JSON.stringify(msg));
const ts = Math.round(collator.calculateTimestamp(currentTime, targetTime) * 1000);
const message = new OSC.Message('/dirt/play', ...keyvals);
const bundle = new OSC.Bundle([message], ts);
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
osc.send(bundle);
}
/**
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "1.3.0",
"version": "1.2.10",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"bin": "./server.js",
@@ -38,8 +38,7 @@
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"osc": "^2.4.5",
"ws": "^8.18.3"
"osc-js": "^2.4.1"
},
"devDependencies": {
"pkg": "^5.8.1",
Executable → Regular
+58 -47
View File
@@ -6,59 +6,70 @@ 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 OSC from 'osc-js';
import OSC from 'osc-js';
import { WebSocketServer } from 'ws';
import osc from 'osc';
const args = process.argv.slice(2);
function getArgValue(flag) {
const i = args.indexOf(flag);
if (i !== -1) {
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
if (nextIsFlag) return true;
return args[i + 1];
}
}
const WS_PORT = 8080; // WebSocket server port
const OSC_REMOTE_IP = '127.0.0.1';
const OSC_REMOTE_PORT = 57120;
let udpClientPort = Number(getArgValue('--port')) || 57120;
let debug = Number(getArgValue('--debug')) || 0;
const udpPort = new osc.UDPPort({
localAddress: '0.0.0.0',
localPort: 0,
remoteAddress: OSC_REMOTE_IP,
remotePort: OSC_REMOTE_PORT,
});
const config = {
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
udpServer: {
host: 'localhost', // @param {string} Hostname of udp server to bind to
port: 57121, // @param {number} Port of udp client for messaging
// enabling the following line will receive tidal messages:
// port: 57120, // @param {number} Port of udp client for messaging
exclusive: false, // @param {boolean} Exclusive flag
},
udpClient: {
host: 'localhost', // @param {string} Hostname of udp client for messaging
port: udpClientPort, // @param {number} Port of udp client for messaging
},
wsServer: {
host: 'localhost', // @param {string} Hostname of WebSocket server
port: 8080, // @param {number} Port of WebSocket server
},
};
udpPort.open();
console.log(`[Sending OSC] ${OSC_REMOTE_IP}:${OSC_REMOTE_PORT}`);
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
udpPort.on('error', (e) => {
console.log('Error: ', e);
});
const wss = new WebSocketServer({ port: WS_PORT });
console.log(`[Listening WS] ws://localhost:${WS_PORT}`);
wss.on('connection', (ws) => {
console.log('New WebSocket connection');
ws.on('message', (message) => {
let osc_host = '127.0.0.1';
let osc_port = 57120;
try {
const data = JSON.parse(message);
if ('host' in data) {
osc_host = data['host'];
}
if ('port' in data) {
osc_port = data['port'];
}
let msg = { address: data['address'], args: data['args'] };
if ('timestamp' in data) {
msg = { timeTag: osc.timeTag(0, data['timestamp']), packets: [msg] };
}
udpPort.send(msg, osc_host, osc_port);
} catch (err) {
console.error('Error parsing message:', err);
if (debug) {
osc.on('*', (message) => {
const { address, args } = message;
let str = '';
for (let i = 0; i < args.length; i += 2) {
str += `${args[i]}: ${args[i + 1]} `;
}
console.log(`${address} ${str}`);
});
}
ws.on('close', () => {
console.log('WebSocket connection closed');
});
osc.on('error', (message) => {
if (message.toString().includes('EADDRINUSE')) {
console.log(`------ ERROR -------
a server is already running on port 57121! to stop it:
1. run "lsof -ti :57121 | xargs kill -9" (macos / linux)
2. re-run the osc server
`);
} else {
console.log(message);
}
});
osc.open();
console.log('osc client running on port', config.udpClient.port);
console.log('osc server running on port', config.udpServer.port);
console.log('websocket server running on port', config.wsServer.port);
if (debug) {
console.log('debug logs enabled. incoming messages will appear below');
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.2.7",
"version": "1.2.6",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "1.2.5",
"version": "1.2.4",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "1.2.6",
"version": "1.2.5",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
-1
View File
@@ -153,7 +153,6 @@ samples('github:tidalcycles/dirt-samples')
The format is `github:<user>/<repo>/<branch>`.
If `<repo>` and `<branch>` are not specified, they will default to `samples` and `main` respectively.
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
The format is also expected to be the same as explained above.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "1.2.6",
"version": "1.2.5",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
+6 -11
View File
@@ -121,18 +121,13 @@ function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let path = base.slice('github:'.length);
let [_, path] = base.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
let components = path.split('/');
let user = components[0];
let repo = components.length >= 2 ? components[1] : 'samples';
let branch = components.length >= 3 ? components[2] : 'main';
let other = components.slice(3);
other.push(subpath ? subpath : '');
other = other.join('/');
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
}
return `https://raw.githubusercontent.com/${path}/${subpath}`;
}
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
-1
View File
@@ -630,7 +630,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// effects
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
if (tremolosync != null) {
+2 -2
View File
@@ -231,12 +231,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
begin: t,
end: envEnd,
frequency,
freqspread: value.detune,
detune: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1),
panspread: value.spread,
spread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
},
{ outputChannelCount: [2] },
+13 -17
View File
@@ -1050,15 +1050,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
return [
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
{ name: 'detune', defaultValue: 0 },
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
{ name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 },
{ name: 'detune', defaultValue: 0.18 },
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
{ name: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, min: 1 },
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
{ name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 },
{ name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 },
];
}
@@ -1236,12 +1235,10 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
_sampleFrame(frame, phase) {
const len = frame.length;
const pos = phase * len;
let i = pos | 0;
if (i >= len) i = 0; // fast wrap
const i = pos | 0;
const frac = pos - i;
const a = frame[i];
let i1 = i + 1;
if (i1 >= len) i1 = 0;
const i1 = i + 1 < len ? i + 1 : 0; // fast wrap
const b = frame[i1];
return a + (b - a) * frac;
}
@@ -1271,7 +1268,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
}
for (let i = 0; i < outL.length; i++) {
const detune = pv(parameters.detune, i);
const freqspread = pv(parameters.freqspread, i);
const tablePos = clamp(pv(parameters.position, i), 0, 1);
const idx = tablePos * (this.numFrames - 1);
const fIdx = idx | 0;
@@ -1280,9 +1276,9 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
const warpMode = pv(parameters.warpMode, i);
const voices = pv(parameters.voices, i);
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0;
const gain1 = Math.sqrt(0.5 - 0.5 * spread);
const gain2 = Math.sqrt(0.5 + 0.5 * spread);
let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 1 / Math.sqrt(voices);
@@ -1295,7 +1291,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
gainL = gain2;
gainR = gain1;
}
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
const dPhase = fVoice * this.invSR;
const level = this._chooseMip(dPhase);
const table = this.tables[level];
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "supradough",
"version": "1.2.4",
"version": "1.2.3",
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
"main": "index.mjs",
"type": "module",
@@ -32,5 +32,6 @@
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*",
"wav-encoder": "^1.3.0"
}
},
"dependencies": {}
}
+2 -2
View File
@@ -1,9 +1,9 @@
import './tonal.mjs';
import './voicings.mjs';
import './ireal.mjs';
export * from './tonal.mjs';
export * from './voicings.mjs';
export * from './ireal.mjs';
import './ireal.mjs';
export const packageName = '@strudel/tonal';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "1.2.5",
"version": "1.2.4",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
"version": "1.2.5",
"version": "1.2.4",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
"version": "1.2.6",
"version": "1.2.5",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
"version": "1.2.6",
"version": "1.2.5",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
"version": "1.2.5",
"version": "1.2.4",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"type": "module",
+8 -246
View File
@@ -41,7 +41,7 @@ importers:
version: 2.2.7
'@vitest/coverage-v8':
specifier: 3.0.4
version: 3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))
version: 3.0.4(vitest@3.0.4)
'@vitest/ui':
specifier: ^3.0.4
version: 3.0.4(vitest@3.0.4)
@@ -212,21 +212,12 @@ importers:
'@strudel/draw':
specifier: workspace:*
version: link:../draw
'@strudel/tonal':
specifier: workspace:*
version: link:../tonal
'@strudel/transpiler':
specifier: workspace:*
version: link:../transpiler
'@tonaljs/tonal':
specifier: ^4.10.0
version: 4.10.0
nanostores:
specifier: ^0.11.3
version: 0.11.3
superdough:
specifier: workspace:*
version: link:../superdough
devDependencies:
vite:
specifier: ^6.0.11
@@ -415,12 +406,9 @@ importers:
'@strudel/core':
specifier: workspace:*
version: link:../core
osc:
specifier: ^2.4.5
version: 2.4.5
ws:
specifier: ^8.18.3
version: 8.18.3
osc-js:
specifier: ^2.4.1
version: 2.4.1
devDependencies:
pkg:
specifier: ^5.8.1
@@ -2432,70 +2420,6 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@serialport/binding-mock@10.2.2':
resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==}
engines: {node: '>=12.0.0'}
'@serialport/bindings-cpp@12.0.1':
resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==}
engines: {node: '>=16.0.0'}
'@serialport/bindings-interface@1.2.2':
resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==}
engines: {node: ^12.22 || ^14.13 || >=16}
'@serialport/parser-byte-length@12.0.0':
resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==}
engines: {node: '>=12.0.0'}
'@serialport/parser-cctalk@12.0.0':
resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==}
engines: {node: '>=12.0.0'}
'@serialport/parser-delimiter@11.0.0':
resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==}
engines: {node: '>=12.0.0'}
'@serialport/parser-delimiter@12.0.0':
resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==}
engines: {node: '>=12.0.0'}
'@serialport/parser-inter-byte-timeout@12.0.0':
resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==}
engines: {node: '>=12.0.0'}
'@serialport/parser-packet-length@12.0.0':
resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==}
engines: {node: '>=8.6.0'}
'@serialport/parser-readline@11.0.0':
resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==}
engines: {node: '>=12.0.0'}
'@serialport/parser-readline@12.0.0':
resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==}
engines: {node: '>=12.0.0'}
'@serialport/parser-ready@12.0.0':
resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==}
engines: {node: '>=12.0.0'}
'@serialport/parser-regex@12.0.0':
resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==}
engines: {node: '>=12.0.0'}
'@serialport/parser-slip-encoder@12.0.0':
resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==}
engines: {node: '>=12.0.0'}
'@serialport/parser-spacepacket@12.0.0':
resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==}
engines: {node: '>=12.0.0'}
'@serialport/stream@12.0.0':
resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
engines: {node: '>=12.0.0'}
'@shikijs/core@1.29.1':
resolution: {integrity: sha512-Mo1gGGkuOYjDu5H8YwzmOuly9vNr8KDVkqj9xiKhhhFS8jisAtDSEWB9hzqRHLVQgFdA310e8XRJcW4tYhRB2A==}
@@ -3748,15 +3672,6 @@ packages:
supports-color:
optional: true
debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'}
@@ -5366,9 +5281,6 @@ packages:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
long@4.0.0:
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@@ -5752,9 +5664,6 @@ packages:
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -5813,9 +5722,6 @@ packages:
resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==}
engines: {node: '>=10'}
node-addon-api@7.0.0:
resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
node-addon-api@8.3.0:
resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==}
engines: {node: ^18 || ^20 || >= 21}
@@ -5854,10 +5760,6 @@ packages:
resolution: {integrity: sha512-yqkmYrMbK1wPrfz7mgeYvA4tBperLg9FQ4S3Sau3nSAkpOA0x0zC8nQ1siBwozy1f4SE8vq2n1WKv99r+PCa1Q==}
engines: {node: '>= 0.6.0'}
node-gyp-build@4.6.0:
resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
hasBin: true
node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
@@ -6016,8 +5918,8 @@ packages:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
osc@2.4.5:
resolution: {integrity: sha512-Nc4/qcl+vA/CMxiKS1xrYgzjfnyB3W94gZnrkn3eTzihlndbEml6+wj1YQNKBI4r+qrw3obCcEoU7SVH/OxpxA==}
osc-js@2.4.1:
resolution: {integrity: sha512-QlSeRKJclL47FNvO1MUCAAp9frmCF9zcYbnf6R9HpcklAst8ZyX3ISsk1v/Vghr/5GmXn0bhVjFXF9h+hfnl4Q==}
own-keys@1.0.1:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
@@ -6778,10 +6680,6 @@ packages:
serialize-javascript@4.0.0:
resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==}
serialport@12.0.0:
resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
engines: {node: '>=16.0.0'}
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
@@ -6869,9 +6767,6 @@ packages:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
slip@1.0.2:
resolution: {integrity: sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g==}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@@ -7747,9 +7642,6 @@ packages:
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
engines: {node: '>=18'}
wolfy87-eventemitter@5.2.9:
resolution: {integrity: sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw==}
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@@ -7872,18 +7764,6 @@ packages:
utf-8-validate:
optional: true
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xmlcreate@2.0.4:
resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==}
@@ -9880,76 +9760,6 @@ snapshots:
'@rtsao/scc@1.1.0': {}
'@serialport/binding-mock@10.2.2':
dependencies:
'@serialport/bindings-interface': 1.2.2
debug: 4.4.0
transitivePeerDependencies:
- supports-color
optional: true
'@serialport/bindings-cpp@12.0.1':
dependencies:
'@serialport/bindings-interface': 1.2.2
'@serialport/parser-readline': 11.0.0
debug: 4.3.4
node-addon-api: 7.0.0
node-gyp-build: 4.6.0
transitivePeerDependencies:
- supports-color
optional: true
'@serialport/bindings-interface@1.2.2':
optional: true
'@serialport/parser-byte-length@12.0.0':
optional: true
'@serialport/parser-cctalk@12.0.0':
optional: true
'@serialport/parser-delimiter@11.0.0':
optional: true
'@serialport/parser-delimiter@12.0.0':
optional: true
'@serialport/parser-inter-byte-timeout@12.0.0':
optional: true
'@serialport/parser-packet-length@12.0.0':
optional: true
'@serialport/parser-readline@11.0.0':
dependencies:
'@serialport/parser-delimiter': 11.0.0
optional: true
'@serialport/parser-readline@12.0.0':
dependencies:
'@serialport/parser-delimiter': 12.0.0
optional: true
'@serialport/parser-ready@12.0.0':
optional: true
'@serialport/parser-regex@12.0.0':
optional: true
'@serialport/parser-slip-encoder@12.0.0':
optional: true
'@serialport/parser-spacepacket@12.0.0':
optional: true
'@serialport/stream@12.0.0':
dependencies:
'@serialport/bindings-interface': 1.2.2
debug: 4.3.4
transitivePeerDependencies:
- supports-color
optional: true
'@shikijs/core@1.29.1':
dependencies:
'@shikijs/engine-javascript': 1.29.1
@@ -10554,7 +10364,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@3.0.4(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0))':
'@vitest/coverage-v8@3.0.4(vitest@3.0.4)':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -11462,11 +11272,6 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.3.4:
dependencies:
ms: 2.1.2
optional: true
debug@4.4.0:
dependencies:
ms: 2.1.3
@@ -13336,8 +13141,6 @@ snapshots:
chalk: 4.1.2
is-unicode-supported: 0.1.0
long@4.0.0: {}
longest-streak@3.1.0: {}
loupe@3.1.2: {}
@@ -14015,9 +13818,6 @@ snapshots:
ms@2.0.0: {}
ms@2.1.2:
optional: true
ms@2.1.3: {}
multimatch@5.0.0:
@@ -14067,9 +13867,6 @@ snapshots:
dependencies:
semver: 7.6.3
node-addon-api@7.0.0:
optional: true
node-addon-api@8.3.0: {}
node-domexception@1.0.0: {}
@@ -14096,9 +13893,6 @@ snapshots:
node-getopt@0.3.2: {}
node-gyp-build@4.6.0:
optional: true
node-gyp-build@4.8.4: {}
node-gyp@10.3.1:
@@ -14350,17 +14144,11 @@ snapshots:
os-tmpdir@1.0.2: {}
osc@2.4.5:
osc-js@2.4.1:
dependencies:
long: 4.0.0
slip: 1.0.2
wolfy87-eventemitter: 5.2.9
ws: 8.18.0
optionalDependencies:
serialport: 12.0.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
own-keys@1.0.1:
@@ -15255,26 +15043,6 @@ snapshots:
dependencies:
randombytes: 2.1.0
serialport@12.0.0:
dependencies:
'@serialport/binding-mock': 10.2.2
'@serialport/bindings-cpp': 12.0.1
'@serialport/parser-byte-length': 12.0.0
'@serialport/parser-cctalk': 12.0.0
'@serialport/parser-delimiter': 12.0.0
'@serialport/parser-inter-byte-timeout': 12.0.0
'@serialport/parser-packet-length': 12.0.0
'@serialport/parser-readline': 12.0.0
'@serialport/parser-ready': 12.0.0
'@serialport/parser-regex': 12.0.0
'@serialport/parser-slip-encoder': 12.0.0
'@serialport/parser-spacepacket': 12.0.0
'@serialport/stream': 12.0.0
debug: 4.3.4
transitivePeerDependencies:
- supports-color
optional: true
set-blocking@2.0.0: {}
set-function-length@1.2.2:
@@ -15417,8 +15185,6 @@ snapshots:
slash@3.0.0: {}
slip@1.0.2: {}
smart-buffer@4.2.0: {}
socks-proxy-agent@8.0.5:
@@ -16328,8 +16094,6 @@ snapshots:
dependencies:
string-width: 7.2.0
wolfy87-eventemitter@5.2.9: {}
word-wrap@1.2.5: {}
wordwrap@0.0.3: {}
@@ -16529,8 +16293,6 @@ snapshots:
ws@8.18.0: {}
ws@8.18.3: {}
xmlcreate@2.0.4: {}
xtend@4.0.2: {}
+16 -61
View File
@@ -1053,10 +1053,10 @@ exports[`runs examples > example "begin" example index 0 1`] = `
exports[`runs examples > example "berlin" example index 0 1`] = `
[
"[ 0/1 → 1/16 | note:A3 ]",
"[ 1/16 → 1/8 | note:Bb3 ]",
"[ 1/8 → 3/16 | note:C4 ]",
"[ 3/16 → 1/4 | note:D4 ]",
"[ 0/1 → 1/16 | note:D3 ]",
"[ 1/16 → 1/8 | note:E3 ]",
"[ 1/8 → 3/16 | note:F3 ]",
"[ 3/16 → 1/4 | note:G3 ]",
"[ 1/4 → 5/16 | note:A3 ]",
"[ 5/16 → 3/8 | note:C4 ]",
"[ 3/8 → 7/16 | note:D4 ]",
@@ -6958,24 +6958,6 @@ exports[`runs examples > example "orbit" example index 0 1`] = `
]
`;
exports[`runs examples > example "oschost" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
"[ 1/1 → 2/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
"[ 2/1 → 3/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
"[ 3/1 → 4/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
]
`;
exports[`runs examples > example "oscport" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
"[ 1/1 → 2/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
"[ 2/1 → 3/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
"[ 3/1 → 4/1 | note:c4 oschost:127.0.0.1 oscport:57120 ]",
]
`;
exports[`runs examples > example "outside" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:A3 ]",
@@ -7150,18 +7132,18 @@ exports[`runs examples > example "penv" example index 0 1`] = `
exports[`runs examples > example "perlin" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh cutoff:4250 ]",
"[ 0/1 → 1/4 | s:bd cutoff:4250 ]",
"[ 1/8 → 1/4 | s:hh cutoff:4252.352717325493 ]",
"[ 1/4 → 3/8 | s:hh cutoff:4265.171895756794 ]",
"[ 1/4 → 1/2 | s:bd cutoff:4265.171895756794 ]",
"[ 3/8 → 1/2 | s:hh cutoff:4290.3361308769945 ]",
"[ 1/2 → 5/8 | s:hh cutoff:4323.283119127154 ]",
"[ 1/2 → 3/4 | s:bd cutoff:4323.283119127154 ]",
"[ 5/8 → 3/4 | s:hh cutoff:4356.230107377314 ]",
"[ 3/4 → 7/8 | s:hh cutoff:4381.394342497515 ]",
"[ 3/4 → 1/1 | s:bd cutoff:4381.394342497515 ]",
"[ 7/8 → 1/1 | s:hh cutoff:4394.213520928815 ]",
"[ 0/1 → 1/8 | s:hh cutoff:500 ]",
"[ 0/1 → 1/4 | s:bd cutoff:500 ]",
"[ 1/8 → 1/4 | s:hh cutoff:562.5486401770559 ]",
"[ 1/4 → 3/8 | s:hh cutoff:903.3554895067937 ]",
"[ 1/4 → 1/2 | s:bd cutoff:903.3554895067937 ]",
"[ 3/8 → 1/2 | s:hh cutoff:1572.364329119182 ]",
"[ 1/2 → 5/8 | s:hh cutoff:2448.2831191271544 ]",
"[ 1/2 → 3/4 | s:bd cutoff:2448.2831191271544 ]",
"[ 5/8 → 3/4 | s:hh cutoff:3324.2019091351267 ]",
"[ 3/4 → 7/8 | s:hh cutoff:3993.210748747515 ]",
"[ 3/4 → 1/1 | s:bd cutoff:3993.210748747515 ]",
"[ 7/8 → 1/1 | s:hh cutoff:4334.017598077253 ]",
"[ 1/1 → 9/8 | s:hh cutoff:4396.566238254309 ]",
"[ 1/1 → 5/4 | s:bd cutoff:4396.566238254309 ]",
"[ 9/8 → 5/4 | s:hh cutoff:4449.536839055554 ]",
@@ -8325,33 +8307,6 @@ exports[`runs examples > example "repeatCycles" example index 0 1`] = `
]
`;
exports[`runs examples > example "replicate" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd ]",
"[ 1/8 → 1/4 | s:bd ]",
"[ 3/8 → 1/2 | s:cp ]",
"[ 1/2 → 5/8 | s:bd ]",
"[ 5/8 → 3/4 | s:bd ]",
"[ 7/8 → 1/1 | s:cp ]",
"[ 1/1 → 9/8 | s:bd ]",
"[ 5/4 → 11/8 | s:sd ]",
"[ 3/2 → 13/8 | s:bd ]",
"[ 13/8 → 7/4 | s:bd ]",
"[ 15/8 → 2/1 | s:cp ]",
"[ 2/1 → 17/8 | s:bd ]",
"[ 17/8 → 9/4 | s:bd ]",
"[ 19/8 → 5/2 | s:cp ]",
"[ 5/2 → 21/8 | s:bd ]",
"[ 11/4 → 23/8 | s:sd ]",
"[ 3/1 → 25/8 | s:bd ]",
"[ 25/8 → 13/4 | s:bd ]",
"[ 27/8 → 7/2 | s:cp ]",
"[ 7/2 → 29/8 | s:bd ]",
"[ 29/8 → 15/4 | s:bd ]",
"[ 31/8 → 4/1 | s:cp ]",
]
`;
exports[`runs examples > example "reset" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
File diff suppressed because it is too large Load Diff
+155
View File
@@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<svg
version="1.0"
id="Layer_1"
width="284.46"
height="284.46"
viewBox="0 0 284.46 284.46"
overflow="visible"
enable-background="new 0 0 284.46 284.46"
xml:space="preserve"
sodipodi:version="0.32"
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
sodipodi:docname="encoder_disc.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
id="metadata2068"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs2066">
</defs><sodipodi:namedview
inkscape:window-height="1131"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
inkscape:zoom="1.0549412"
inkscape:cx="195.27154"
inkscape:cy="-101.90141"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:current-layer="Layer_1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:window-maximized="1" />
<path
id="path1"
style="fill:#f9f9f9;stroke:none;stroke-width:1.23072"
d="M 142.22902,0.61574073 A 141.61462,141.61462 0 0 0 0.61574173,142.22901 141.61462,141.61462 0 0 0 142.22902,283.84425 141.61462,141.61462 0 0 0 283.84425,142.22901 141.61462,141.61462 0 0 0 142.22902,0.61574073 Z m 0,106.22069927 a 35.393924,35.393924 0 0 1 35.39453,35.39257 35.393924,35.393924 0 0 1 -35.39453,35.39455 35.393924,35.393924 0 0 1 -35.39258,-35.39455 35.393924,35.393924 0 0 1 35.39258,-35.39257 z" /><g
id="g2"><path
d="M 242.47,41.99 242.441,42.02 217.4,67.06 C 198.17,47.83 171.589,35.93 142.23,35.93 V 0.5 c 39.17,0 74.6,15.85 100.24,41.49 z"
id="path2015" /><path
d="m 142.23,0.5 v 35.43 c -29.37,0 -55.93,11.89 -75.17,31.13 L 42.02,42.02 41.99,41.99 C 67.63,16.35 103.06,0.5 142.23,0.5 Z"
id="path2017" /><path
d="m 142.23,35.93 v 35.43 c -19.59,0 -37.3,7.92 -50.12,20.75 L 67.06,67.06 C 86.3,47.82 112.86,35.93 142.23,35.93 Z"
id="path2019" /><path
d="M 67.06,67.06 92.11,92.11 C 79.28,104.93 71.36,122.64 71.36,142.23 H 35.93 c 0,-29.36 11.89,-55.94 31.13,-75.17 z"
id="path2021" /><path
d="M 92.11,192.35 67.06,217.4 C 47.82,198.171 35.93,171.59 35.93,142.23 h 35.43 c 0,19.589 7.92,37.3 20.75,50.12 z"
id="path2023" /><path
d="m 142.23,213.09 v 35.44 c -29.37,0 -55.93,-11.891 -75.17,-31.131 l 25.05,-25.05 c 12.82,12.821 30.53,20.741 50.12,20.741 z"
id="path2025" /><path
d="m 142.23,248.53 v 35.43 c -39.17,0 -74.6,-15.851 -100.24,-41.49 l 0.03,-0.03 25.04,-25.04 c 19.24,19.24 45.8,31.13 75.17,31.13 z"
id="path2027" /><path
d="m 142.23,283.96 v -35.43 c 29.36,0 55.94,-11.9 75.17,-31.131 l 25.04,25.04 0.029,0.03 C 216.83,268.109 181.4,283.96 142.23,283.96 Z"
id="path2029" /><path
d="m 177.66,142.229 h 35.43 c 0,19.59 -7.92,37.301 -20.74,50.12 L 167.3,167.3 c 6.4,-6.401 10.36,-15.25 10.36,-25.071 z"
id="path2031" /><path
d="m 167.3,167.3 25.05,25.05 c -12.819,12.82 -30.529,20.74 -50.12,20.74 v -35.43 c 9.8,0 18.66,-3.95 25.07,-10.36 z"
id="path2033" /><path
d="m 142.23,177.66 v 35.43 c -19.59,0 -37.3,-7.92 -50.12,-20.74 l 25.05,-25.04 c 6.42,6.41 15.28,10.35 25.07,10.35 z"
id="path2035" /><path
d="m 117.16,167.3 v 0.01 L 92.11,192.35 C 79.28,179.531 71.36,161.82 71.36,142.23 h 35.43 c 0,9.82 3.96,18.669 10.37,25.07 z"
id="path2037" /><path
fill="none"
stroke="#000000"
d="m 142.23,283.96 c -39.17,0 -74.6,-15.851 -100.24,-41.49 C 16.35,216.83 0.5,181.399 0.5,142.229 0.5,103.059 16.35,67.629 41.99,41.989 67.63,16.35 103.06,0.5 142.23,0.5 c 39.17,0 74.6,15.85 100.24,41.49 25.641,25.64 41.49,61.07 41.49,100.24 0,39.17 -15.85,74.601 -41.49,100.24 -25.64,25.639 -61.07,41.49 -100.24,41.49 z"
id="path2041" /><path
fill="none"
stroke="#000000"
d="m 248.53,142.229 c 0,-29.35 -11.891,-55.93 -31.13,-75.17 -19.23,-19.23 -45.811,-31.13 -75.17,-31.13 -29.37,0 -55.93,11.89 -75.17,31.13 -19.24,19.23 -31.13,45.81 -31.13,75.17 0,29.36 11.89,55.94 31.13,75.17 19.24,19.24 45.8,31.131 75.17,31.131 29.36,0 55.94,-11.9 75.17,-31.131 19.24,-19.239 31.13,-45.819 31.13,-75.17 z"
id="path2043" /><path
fill="none"
stroke="#000000"
d="m 142.23,213.09 c -19.59,0 -37.3,-7.92 -50.12,-20.74 -12.83,-12.819 -20.75,-30.53 -20.75,-50.12 0,-19.59 7.92,-37.3 20.75,-50.12 12.82,-12.83 30.53,-20.75 50.12,-20.75 19.59,0 37.3,7.92 50.12,20.75 12.82,12.82 20.74,30.53 20.74,50.12 0,19.59 -7.92,37.301 -20.74,50.12 -12.82,12.82 -30.53,20.74 -50.12,20.74 z"
id="path2045" /><path
fill="none"
stroke="#000000"
d="m 117.16,167.31 c 6.42,6.41 15.28,10.351 25.07,10.351 9.8,0 18.66,-3.95 25.07,-10.36 6.4,-6.4 10.36,-15.25 10.36,-25.07 0,-9.819 -3.95,-18.67 -10.36,-25.07 -6.399,-6.41 -15.27,-10.36 -25.07,-10.36 -9.8,0 -18.66,3.95 -25.08,10.35 -6.4,6.4 -10.36,15.26 -10.36,25.08 0,9.82 3.96,18.67 10.37,25.07"
id="path2047" /><path
id="polyline2049"
style="fill:none;stroke:#000000"
d="m 142.23,177.66 v 35.43 35.44 35.43"
sodipodi:nodetypes="cccc" /><path
id="polyline2051"
style="fill:none;stroke:#000000"
d="m 167.3,167.3 25.05,25.05 25.05,25.049 25.04,25.04"
sodipodi:nodetypes="cccc" /><path
id="polyline2053"
style="fill:none;stroke:#000000"
d="m 177.66,142.229 h 35.43 35.44 35.43"
sodipodi:nodetypes="cccc" /><path
id="polyline2055"
style="fill:none;stroke:#000000"
d="M 167.3,117.16 192.35,92.11 217.4,67.06 242.44,42.02"
sodipodi:nodetypes="cccc" /><path
id="polyline2057"
style="fill:none;stroke:#000000"
d="M 142.23,106.8 V 71.36 35.93 0.5"
sodipodi:nodetypes="cccc" /><path
id="polyline2059"
style="fill:none;stroke:#000000"
d="M 117.15,117.15 92.11,92.11 67.06,67.06 42.02,42.02"
sodipodi:nodetypes="cccc" /><path
id="polyline2061"
style="fill:none;stroke:#000000"
d="M 106.79,142.229 H 71.36 35.93 0.5"
sodipodi:nodetypes="cccc" /><path
id="polyline2063"
style="fill:none;stroke:#000000"
d="m 117.16,167.3 v 0.01 l -25.05,25.04 -25.05,25.049 -25.04,25.04"
sodipodi:nodetypes="ccccc" /></g>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

+68
View File
@@ -0,0 +1,68 @@
import React, { useState, useRef, useEffect } from 'react';
export default function Tap({ initialCps = 0.4, maxSamples = 3 }) {
const [timestamps, setTimestamps] = useState([]);
const [cps, setCps] = useState(initialCps);
function addTap(ts = Date.now()) {
setTimestamps((prev) => {
const next = [...prev, ts].slice(-(maxSamples + 1));
calcCps(next);
return next;
});
}
function calcCps(times) {
if (!times || times.length < 2) return;
const intervals = [];
for (let i = 1; i < times.length; i++) {
intervals.push(times[i] - times[i - 1]);
}
const avgMs = intervals.reduce((a, b) => a + b, 0) / intervals.length;
const newCps = 1000 / avgMs;
if (Number.isFinite(newCps) && newCps > 0 && newCps < 1000) {
setCps(newCps);
}
}
function handleTap(e) {
e && e.preventDefault();
addTap();
}
function handleReset(e) {
e && e.preventDefault();
reset();
}
useEffect(() => {
function onKey(e) {
if (e.code === 'Space') {
e.preventDefault();
addTap();
} else if (e.code === 'Backspace') {
e.preventDefault();
reset();
}
}
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
function reset() {
setTimestamps([]);
// setCps(initialCps);
}
return (
<div>
<div class="flex flex-col items-center p-7 font-medium text-white">
<div>{cps.toFixed(2)} cps</div>
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" onClick={handleTap}>
Tap
</button>
</div>
</div>
);
}
+59
View File
@@ -0,0 +1,59 @@
/*
Vinyl.jsx - <short description TODO>
Copyright (C) 2025 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/App.js>
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 React, { useState, useEffect } from 'react';
import { setInterval, clearInterval } from 'worker-timers';
import { NeoCyclist } from '@strudel/core/neocyclist.mjs';
import { getAudioContext } from '@strudel/webaudio';
import { saw } from '@strudel/core';
import encoder from '../../public/encoder_disc.svg';
console.log(encoder);
const schedulerOptions = {
onTrigger: (x) => {},
getTime: () => getAudioContext().currentTime,
onToggle: (started) => console.log('started: ', started),
setInterval,
clearInterval,
// beforeStart,
};
const cyclist = new NeoCyclist(schedulerOptions);
export function Vinyl() {
const [isActive, setIsActive] = useState(false);
const [cyclepos, setCyclepos] = useState(0);
const activate = () => setIsActive(!isActive);
if (isActive) {
if (!cyclist.started) {
cyclist.start();
// cyclist.setPattern(saw.segment(8));
}
} else {
cyclist.stop();
}
useEffect(() => {
const intervalId = setInterval(() => {
setCyclepos(cyclist.cycle);
}, 10);
return () => clearInterval(intervalId);
}, []);
const deg = (cyclepos % 1) * 360;
const style = {
fontSize: '20em',
transform: 'rotate(' + deg + 'deg)',
};
return (
<div onClick={activate} style={style}>
<center>
<img src={encoder.src} />
</center>
</div>
);
}
+17
View File
@@ -0,0 +1,17 @@
---
import HeadCommon from '../components/HeadCommon.astro';
import { Vinyl } from '../components/Vinyl.jsx';
import Tap from '../components/Tap.jsx';
---
<html lang="en" class="m-0 dark">
<head>
<HeadCommon />
<title>Strudel Cycler</title>
</head>
<body class="h-app-height bg-background m-0">
<div>Hello there.</div>
<Vinyl client:only="react" />
<Tap client:only="react" />
</body>
</html>
-17
View File
@@ -60,23 +60,6 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp
.room(0.5)`}
/>
## Write your own chained function
You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function.
<MiniRepl
client:idle
tune={`const effectChain = register('effectChain', (pat) => pat
.s("sawtooth")
.cutoff(500)
//.delay(0.5)
.room(0.5)
)
note("a3 c#4 e4 a4").effectChain()`}
/>
Try adding `.rev()` after `effectChain()` to hear further effects added.
# Comments
The `//` in the example above is a line comment, resulting in the `delay` function being ignored.
+1 -1
View File
@@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample
### scrub
<JsDoc client:idle name="Pattern.scrub" h={0} />
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
### speed
+1 -1
View File
@@ -157,7 +157,7 @@ It has 20 parameters in total, here is a snippet that uses all:
.pitchJump(0) // +/- pitch change after pitchJumpTime
.pitchJumpTime(0) // >0 time after pitchJump is applied
.lfo(0) // >0 resets slide + pitchJump + sets tremolo speed
.tremolo(0.5) // 0-1 lfo volume modulation amount
.tremolo(0) // 0-1 lfo volume modulation amount
//.duration(.2) // overwrite strudel event duration
//.gain(1) // change volume
._scope() // vizualise waveform (not zzfx related)
+8 -9
View File
@@ -6,7 +6,6 @@ import './files.mjs';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
const baseCDN = 'https://strudel.b-cdn.net';
export async function prebake() {
// https://archive.org/details/SalamanderGrandPianoV3
@@ -20,23 +19,23 @@ export async function prebake() {
// => getting "window is not defined", as soon as "@strudel/soundfonts" is imported statically
// seems to be a problem with soundfont2
import('@strudel/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()),
samples(`${baseCDN}/piano.json`, `${baseCDN}/piano/`, { prebake: true }),
samples(`${baseNoTrailing}/piano.json`, undefined, { prebake: true }),
// https://github.com/sgossner/VCSL/
// https://api.github.com/repositories/126427031/contents/
// LICENSE: CC0 general-purpose
samples(`${baseCDN}/vcsl.json`, `${baseCDN}/VCSL/`, { prebake: true }),
samples(`${baseCDN}/tidal-drum-machines.json`, `${baseCDN}/tidal-drum-machines/machines/`, {
samples(`${baseNoTrailing}/vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }),
samples(`${baseNoTrailing}/tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', {
prebake: true,
tag: 'drum-machines',
}),
samples(`${baseCDN}/uzu-drumkit.json`, `${baseCDN}/uzu-drumkit/`, {
samples(`${baseNoTrailing}/uzu-drumkit.json`, undefined, {
prebake: true,
tag: 'drum-machines',
}),
samples(`${baseCDN}/uzu-wavetables.json`, `${baseCDN}/uzu-wavetables/`, {
samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, {
prebake: true,
}),
samples(`${baseCDN}/mridangam.json`, `${baseCDN}/mrid/`, { prebake: true, tag: 'drum-machines' }),
samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }),
samples(
{
casio: ['casio/high.wav', 'casio/low.wav', 'casio/noise.wav'],
@@ -146,14 +145,14 @@ export async function prebake() {
'num/20.wav',
],
},
`${baseCDN}/Dirt-Samples/`,
'github:tidalcycles/dirt-samples',
{
prebake: true,
},
),
]);
aliasBank(`${baseCDN}/tidal-drum-machines-alias.json`);
aliasBank(`${baseNoTrailing}/tidal-drum-machines-alias.json`);
}
const maxPan = noteToMidi('C8');