Compare commits

..

18 Commits

Author SHA1 Message Date
Jade (Rose) Rowland 39013c09ef sl 2025-11-01 01:13:20 -04:00
Jade (Rose) Rowland 9d425dfff6 flattening 2025-10-22 13:12:41 -04: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
16 changed files with 169 additions and 377 deletions
+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);
+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
@@ -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.
+35 -22
View File
@@ -1,4 +1,4 @@
import { getCommonSampleInfo } from './util.mjs';
import { getCommonSampleInfoFromBank } from './util.mjs';
import { registerSound, registerWaveTable } from './index.mjs';
import { getAudioContext } from './audioContext.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
@@ -23,35 +23,34 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
export function getSampleInfo(hapValue, bank) {
const { speed = 1.0 } = hapValue;
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, url, index, midi, label, playbackRate };
}
// takes hapValue and returns buffer + playbackRate.
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
export const getSampleBuffer = async (label, sampleUrl, resolveUrl) => {
if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl);
}
const ac = getAudioContext();
const buffer = await loadBuffer(sampleUrl, ac, label);
return buffer
};
function getBufferPlaybackRate(hapValue, buffer, transpose) {
const { speed = 1.0 } = hapValue;
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
if (hapValue.unit === 'c') {
playbackRate = playbackRate * buffer.duration;
}
return { buffer, playbackRate };
};
return playbackRate;
}
// creates playback ready AudioBufferSourceNode from hapValue
export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl);
export function getSampleBufferSource(hapValue, buffer,transpose) {
if (hapValue.speed < 0) {
// should this be cached?
buffer = reverseBuffer(buffer);
}
const playbackRate = getBufferPlaybackRate(hapValue, buffer, transpose)
const ac = getAudioContext();
const bufferSource = ac.createBufferSource();
bufferSource.buffer = buffer;
@@ -121,13 +120,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 || '') => {
@@ -257,7 +263,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
const cutGroups = [];
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
export async function onTriggerSample(t, value, onended, bufferSrc) {
let {
s,
nudge = 0, // TODO: is this in seconds?
@@ -279,7 +285,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
// destructure adsr here, because the default should be different for synths and samples
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
const { bufferSource, sliceDuration, offset } = bufferSrc
// asny stuff above took too long?
if (ac.currentTime > t) {
@@ -341,14 +347,21 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
return handle;
}
async function getBufferSrcFromBank(hapValue, bank, resolveUrl = undefined) {
const { transpose, url, label } = getCommonSampleInfoFromBank(hapValue, bank)
let buffer = await getSampleBuffer(label,url, resolveUrl);
return getSampleBufferSource(hapValue, buffer, transpose)
}
function registerSample(key, bank, params) {
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
registerSound(key, async (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, await getBufferSrcFromBank(hapValue, bank, undefined)), {
type: 'sample',
samples: bank,
...params,
});
}
export function registerSampleSource(key, bank, params) {
const isWavetable = key.startsWith('wt_');
if (isWavetable) {
+19 -4
View File
@@ -80,12 +80,13 @@ export function secondsToCycle(t, cps) {
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getCommonSampleInfo(hapValue, bank) {
export function getCommonSampleInfoFromBank(hapValue, bank) {
const { s, n = 0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let url;
let index = 0;
let {transpose, label} = getCommonSampleInfo(hapValue)
if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length);
url = bank[index];
@@ -102,6 +103,20 @@ export function getCommonSampleInfo(hapValue, bank) {
index = getSoundIndex(n, bank[closest].length);
url = bank[closest][index];
}
const label = `${s}:${index}`;
return { transpose, url, index, midi, label };
label = `${s}:${index}`;
return { transpose, url, label };
}
export function getCommonSampleInfo(hapValue) {
const { s, n = 0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
const label = `${s}:${n}`;
return { transpose, label };
}
+4 -4
View File
@@ -1,5 +1,5 @@
import { getAudioContext, registerSound } from './index.mjs';
import { getCommonSampleInfo } from './util.mjs';
import { getCommonSampleInfoFromBank } from './util.mjs';
import {
applyFM,
applyParameterModulators,
@@ -216,7 +216,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
}
const frequency = getFrequencyFromValue(value);
const { url, label } = getCommonSampleInfo(value, tables);
const { url, label } = getCommonSampleInfoFromBank(value, tables);
const payload = await getPayload(url, label, frameLen);
let holdEnd = t + duration;
if (clip !== undefined) {
@@ -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
+67 -11
View File
@@ -1,6 +1,8 @@
import { registerSampleSource } from '@strudel/webaudio';
import { getSampleBufferSource, onTriggerSample, registerSampleSource } from '@strudel/webaudio';
import { isAudioFile } from './files.mjs';
import { logger } from '@strudel/core';
import { getSoundIndex, logger } from '@strudel/core';
import { registerSound } from '@strudel/webaudio';
import { getCommonSampleInfo } from '../../../packages/superdough/util.mjs';
//utilites for writing and reading to the indexdb
@@ -25,6 +27,15 @@ function clearAllIDB() {
export function clearIDB(dbName) {
return window.indexedDB.deleteDatabase(dbName);
}
function registerSampleFromIdb(key, bank, params) {
}
// queries the DB, and registers the sounds so they can be played
@@ -52,31 +63,76 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
if (!isAudioFile(title)) {
return;
}
const splitRelativePath = soundFile.id.split('/');
let parentDirectory =
//fallback to file name before period and seperator if no parent directory
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
const blob = soundFile.blob;
return blobToDataUrl(blob).then((soundPath) => {
// const blob = soundFile.blob;
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
titlePathMap.set(title, soundPath);
titlePathMap.set(title, soundFile.id);
sounds.set(parentDirectory, titlePathMap);
return;
});
// return blobToDataUrl(blob).then((soundPath) => {
// const titlePathMap = sounds.get(parentDirectory) ?? new Map();
// titlePathMap.set(title, soundPath);
// sounds.set(parentDirectory, titlePathMap);
// return;
// });
}),
)
.then(() => {
sounds.forEach((titlePathMap, key) => {
const value = Array.from(titlePathMap.keys())
const bank = Array.from(titlePathMap.keys())
.sort((a, b) => {
return a.localeCompare(b);
})
.map((title) => titlePathMap.get(title));
registerSampleSource(key, value, { prebake: false });
registerSound(key, async (t, hapValue, onended) => {
const { s, n = 0 } = hapValue;
const index = getSoundIndex(n, bank.length);
let {transpose, label} = getCommonSampleInfo(hapValue)
const storeKey = bank[index];
openDB(config, (objectStore) => {
const getRequest = objectStore.get(storeKey);
getRequest.onsuccess = async (event) => {
const result = event.target.result;
let buffer = result?.blob.arrayBuffer ? await result.blob.arrayBuffer() : null;
console.info(buffer)
if (buffer) {
const bufferSource = getSampleBufferSource(hapValue,buffer,transpose)
onTriggerSample(t, hapValue, onended, bufferSource)
} else {
logger(`Could not load sample for ${storeKey}`, 'error');
}
};
})
// const buffer = objectStore.getKey(key)
// const bufferSource = getSampleBufferSource(hapValue,buffer,transpose)
// onTriggerSample(t, hapValue, onended, await getBufferSrcFromBank(hapValue, bank, undefined))
}, {
type: 'sample',
samples: bank,
});
// registerSampleSource(key, value, { prebake: false });
});
logger('imported sounds registered!', 'success');