Compare commits

..

22 Commits

Author SHA1 Message Date
froos 947dd76823 Merge branch 'main' into nkymut/midimap+ 2025-10-20 20:40:03 +02:00
Switch Angel AKA Jade Rose 6e58c973af Merge pull request 'Bug Fix: Wavetable: phase wrapping at 1 and detune' (#1620) from glossing/strudel:glossing/wrap-phase-typo into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1620
2025-10-18 04:29:19 +02:00
Switch Angel AKA Jade Rose 2473a5391d Merge branch 'main' into glossing/wrap-phase-typo 2025-10-17 04:55:25 +02:00
froos 4e17cfbdd6 Merge pull request 'github samples: default to "samples" if repository is not specified' (#1644) from prezmop/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1644
2025-10-16 20:26:26 +02:00
prezmop 83c7e63432 update docs 2025-10-16 18:20:22 +02:00
prezmop 909e0154fe default to samples if repo not found 2025-10-16 18:20:13 +02:00
froos ef4e21ac40 Merge pull request 'Docs: add example of custom chained function' (#1642) from dariusk/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1642
2025-10-16 17:33:39 +02:00
Aria 3c1a8c8bdb Format 2025-10-14 11:41:11 -05:00
Aria b7e941c649 Merge branch 'main' into glossing/wrap-phase-typo 2025-10-14 11:40:29 -05:00
Darius Kazemi 9c5c71c31a Removing semicolon to be more idiomatic;;; 2025-10-11 17:52:26 -07:00
Darius Kazemi af53bab259 Fixing some syntax and a typo 2025-10-11 17:50:58 -07:00
Darius Kazemi 0065db8569 Docs: add example of custom chained function
This adds a subsection to the "Understand/Coding Syntax" documentation that shows a simple example of converting the previous chain of effects into a custom, reusable chained function. There's also a prompt for the reader to experiment.
2025-10-11 17:40:55 -07:00
Aria 6f703e48e7 Merge branch 'main' into glossing/wrap-phase-typo 2025-10-08 16:44:35 -05:00
Aria 1ae773acc4 Merge branch 'main' into glossing/wrap-phase-typo 2025-10-05 18:21:05 -05:00
Aria 120f89d57f Handle detune in the presence of pitch envelope 2025-10-04 01:37:57 -05:00
Aria 99e14dae5c Consistency 2025-10-02 01:12:42 -05:00
Aria ac582b4d40 Wrap properly when phase === 1 2025-10-02 01:06:29 -05:00
nkymut 28eedae730 docs: add tips for enabling MIDI in Chrome for local development 2025-05-21 00:00:35 +08:00
nkymut e075095e8b Add warning when midimap overrides existing Strudel API 2025-05-18 14:48:53 +08:00
nkymut 84e3f12171 Merge branch 'main' of https://github.com/tidalcycles/strudel into midimap+ 2025-05-12 06:15:51 +08:00
nkymut 260c6e0783 Merge branch 'main' of https://github.com/tidalcycles/strudel into midimap+ 2025-03-22 06:04:24 +08:00
nkymut bc3481822c allow midimaps to register arbitary controlnames 2025-02-09 16:38:15 +08:00
20 changed files with 123 additions and 407 deletions
-6
View File
@@ -140,9 +140,3 @@ 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 });
});
+3 -15
View File
@@ -12,8 +12,6 @@ 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();
@@ -41,7 +39,6 @@ const sendTick = (phase, duration, tick, time) => {
cps,
time,
cycle,
max_cycle,
});
num_ticks_since_cps_change++;
};
@@ -63,10 +60,9 @@ const stopClock = async (id) => {
const otherClientStarted = Array.from(clients.values()).some((c) => c.started);
//dont stop the clock if other instances are running...
// actually do stop it
// if (!started || otherClientStarted) {
// return;
// }
if (!started || otherClientStarted) {
return;
}
clock.stop();
setCycle(0);
@@ -78,10 +74,6 @@ const setCycle = (cycle) => {
num_cycles_at_cps_change = cycle;
};
const setMaxCycle = (cycle) => {
max_cycle = cycle;
};
const processMessage = (message) => {
const { type, payload } = message;
@@ -100,10 +92,6 @@ const processMessage = (message) => {
setCycle(payload.cycle);
break;
}
case 'setmaxcycle': {
setMaxCycle(payload.maxcycle);
break;
}
case 'toggle': {
if (payload.started) {
startClock(message.id);
+17
View File
@@ -2320,6 +2320,23 @@ export const { miditouch } = registerControl('miditouch');
// TODO: what is this?
export const { polyTouch } = registerControl('polyTouch');
/**
* Checks if a control name exists in the controlAlias map.
* @name hasControlName
* @param {string} alias The control name to check
* @returns {boolean} True if the control name exists, false otherwise
*/
export const hasControlName = (alias) => {
// Check if the name exists as a key (alias) or value (main control name)
return controlAlias.has(alias) || Array.from(controlAlias.values()).includes(alias);
};
/**
* Gets the control name from the controlAlias map.
* @name getControlName
* @param {string} alias The control name to get
* @returns {string} The control name
*/
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.03,
latency = 0.1,
setInterval,
clearInterval,
beforeStart,
+1 -1
View File
@@ -18,7 +18,7 @@ export function logger(message, type, data = {}) {
}
lastMessage = message;
lastTime = t;
console.log(`${t} %c${message}`, 'background-color: black;color:white;border-radius:15px');
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
document.dispatchEvent(
new CustomEvent(logKey, {
+3 -6
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://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/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,7 +11,6 @@ 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
@@ -20,18 +19,16 @@ 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, maxcycle, time } = payload;
const { cps, begin, end, cycle, 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
View File
@@ -4,4 +4,3 @@ export * from './draw.mjs';
export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './pitchwheel.mjs';
export * from './textbox.mjs';
-50
View File
@@ -1,50 +0,0 @@
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;
};
+8
View File
@@ -8,6 +8,14 @@ This package adds midi functionality to strudel Patterns.
npm i @strudel/midi --save
```
## Enabling MIDI for Local Development in Chrome
1. Open Chrome and navigate to `chrome://flags`
2. Search for "Insecure origins treated as secure"
3. In the text field that appears, add your development origin (e.g., http://localhost:3000)
4. Enable the flag
5. Restart Chrome
## Available Controls
The following MIDI controls are available:
+40 -7
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import * as _WebMidi from 'webmidi';
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core';
import { noteToMidi, hasControlName, getControlName, registerControl } from '@strudel/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
@@ -100,10 +100,34 @@ export const midicontrolMap = new Map();
function unifyMapping(mapping) {
return Object.fromEntries(
Object.entries(mapping).map(([key, mapping]) => {
// Convert number to object with ccn property
if (typeof mapping === 'number') {
mapping = { ccn: mapping };
}
return [getControlName(key), mapping];
// Get the non-aliased control name from the key
const controlName = getControlName(key);
// Check if the key or controlName already exists in the controlAlias map
if (hasControlName(key) || hasControlName(controlName)) {
// Show warning and carry on.
logger(`[midimap] '[${key}, ${controlName}]' overwrites a Strudel API.`);
// Throw error to stop the music
//throw new Error(`[midimap] '${key}' overwrites a Strudel API.`);
}
// Register the control in the midicontrolMap if it doesn't exist
if (!midicontrolMap.has(controlName)) {
try {
registerControl(controlName);
} catch (err) {
throw new Error(`[midimap] Failed to register midimap control '${controlName}': ${err.message}`);
}
} else {
logger(`[midimap] '${controlName}' already registered as a midimap control. Skipping registration.`);
}
return [controlName, mapping];
}),
);
}
@@ -162,7 +186,14 @@ export async function midimaps(map) {
map = await loadCache[map];
}
if (typeof map === 'object') {
Object.entries(map).forEach(([name, mapping]) => midicontrolMap.set(name, unifyMapping(mapping)));
Object.entries(map).forEach(([name, mapping]) => {
try {
midicontrolMap.set(name, unifyMapping(mapping));
} catch (err) {
logger(`[midi] Error setting midimap '${name}': ${err.message}`);
throw err;
}
});
}
}
@@ -324,18 +355,18 @@ Pattern.prototype.midi = function (midiport, options = {}) {
const device = getDevice(midiConfig.midiport, outputs);
const otherOutputs = outputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
`[midi] Midi enabled! Using "${device.name}". ${
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
}`,
);
},
onDisconnected: ({ outputs }) =>
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
logger(`[midi] Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((hap, currentTime, cps, targetTime) => {
if (!WebMidi.enabled) {
logger('Midi not enabled');
logger('[midi] Midi not enabled');
return;
}
hap.ensureObjectValue();
@@ -383,7 +414,9 @@ Pattern.prototype.midi = function (midiport, options = {}) {
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
} else if (midimap !== 'default') {
// Add warning when a non-existent midimap is specified
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
throw new Error(
`[midimap] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`,
);
}
// Handle note
+1
View File
@@ -153,6 +153,7 @@ 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.
+12 -5
View File
@@ -121,13 +121,20 @@ function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let [_, path] = base.split('github:');
let path = base.slice('github:'.length);
path = path.endsWith('/') ? path.slice(0, -1) : path;
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
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);
if (subpath) {
other.push(subpath);
}
return `https://raw.githubusercontent.com/${path}/${subpath}`;
other = other.join('/');
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
}
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
+2 -2
View File
@@ -231,12 +231,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
begin: t,
end: envEnd,
frequency,
detune: value.detune,
freqspread: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1),
spread: value.spread,
panspread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
},
{ outputChannelCount: [2] },
+17 -13
View File
@@ -1050,14 +1050,15 @@ 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: 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: '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: 'warpMode', defaultValue: 0 },
{ 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 },
{ name: 'voices', defaultValue: 1, min: 1 },
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
];
}
@@ -1235,10 +1236,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
_sampleFrame(frame, phase) {
const len = frame.length;
const pos = phase * len;
const i = pos | 0;
let i = pos | 0;
if (i >= len) i = 0; // fast wrap
const frac = pos - i;
const a = frame[i];
const i1 = i + 1 < len ? i + 1 : 0; // fast wrap
let i1 = i + 1;
if (i1 >= len) i1 = 0;
const b = frame[i1];
return a + (b - a) * frac;
}
@@ -1268,6 +1271,7 @@ 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;
@@ -1276,9 +1280,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 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);
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);
let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 1 / Math.sqrt(voices);
@@ -1291,7 +1295,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
gainL = gain2;
gainR = gain1;
}
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
const dPhase = fVoice * this.invSR;
const level = this._chooseMip(dPhase);
const table = this.tables[level];
-155
View File
@@ -1,155 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 6.8 KiB

-68
View File
@@ -1,68 +0,0 @@
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
@@ -1,59 +0,0 @@
/*
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
@@ -1,17 +0,0 @@
---
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,6 +60,23 @@ 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