mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-14 06:43:47 -04:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fbcbf60ed | |||
| 481838c712 | |||
| a6c8c0814a | |||
| 70c932a760 | |||
| f5631a01ae | |||
| 989b5dcc3a | |||
| 5de891f31d | |||
| 325e348bbf | |||
| fe117e23fd | |||
| ded91fab7d | |||
| f40d079543 | |||
| d5833dd475 | |||
| e3658ec30e | |||
| 72eaf96b4f | |||
| 7eebf3a466 | |||
| 64f16c4f33 | |||
| ffbbc43c89 | |||
| 086596c47c | |||
| fdde05e751 | |||
| 694162a7b1 | |||
| 69a82ddc5b | |||
| cbcb25b063 | |||
| 0d08225983 | |||
| 00872bd1de | |||
| d496919fe5 | |||
| f369de13d7 | |||
| b7d98dd370 | |||
| 4209506fd0 | |||
| 0ecacae71e | |||
| ff790c803d | |||
| f0efaabe20 | |||
| 8cd497abb2 | |||
| 070055d2ef | |||
| e500ebe268 | |||
| 72298c5f03 | |||
| c885d84785 | |||
| a14bf4d97b | |||
| c5f1085bd9 | |||
| ccbf28f2db | |||
| 3a7ec18d46 | |||
| a2b8407fbb | |||
| ac1fd88d8d | |||
| 9f8143e062 | |||
| cdc5e5f3f3 | |||
| bad498a1e5 | |||
| 4de0c11f8c | |||
| f360d394a7 | |||
| 07e3ddbe40 | |||
| b3537a9acb | |||
| f0669ce3ba | |||
| 69b034349a | |||
| 5f7e10733a | |||
| 9848740e18 | |||
| 51b150b829 | |||
| 9b1c23d49f | |||
| 42ab65abdb | |||
| bb982e6b9b | |||
| 5097ae8fc0 | |||
| 6b1e6cc7af | |||
| 9c6eca9a59 | |||
| 02bb88bde4 | |||
| c16511dfbd | |||
| 5054756ca2 |
@@ -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);
|
||||
|
||||
@@ -1887,19 +1887,52 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size',
|
||||
export const { shape } = registerControl(['shape', 'shapevol']);
|
||||
/**
|
||||
* Wave shaping distortion. CAUTION: it can get loud.
|
||||
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output.
|
||||
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Third option sets the waveshaping type.
|
||||
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
|
||||
*
|
||||
* @name distort
|
||||
* @synonyms dist
|
||||
* @param {number | Pattern} distortion
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
* @param {number | string | Pattern} type type of distortion to apply
|
||||
* @example
|
||||
* s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
|
||||
* @example
|
||||
* note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4")
|
||||
* @example
|
||||
* s("bd:4*4").bank("tr808").distort("3:0.5:diode")
|
||||
*
|
||||
*/
|
||||
export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist');
|
||||
export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist');
|
||||
|
||||
/**
|
||||
* Postgain for waveshaping distortion.
|
||||
*
|
||||
* @name distortvol
|
||||
* @synonyms distvol
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
* @example
|
||||
* s("bd*4").bank("tr909").distort(2).distortvol(0.8)
|
||||
*/
|
||||
export const { distortvol } = registerControl('distortvol', 'distvol');
|
||||
|
||||
/**
|
||||
* Type of waveshaping distortion to apply.
|
||||
*
|
||||
* @name distorttype
|
||||
* @synonyms disttype
|
||||
* @param {number | string | Pattern} type type of distortion to apply
|
||||
* @example
|
||||
* s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>")
|
||||
*
|
||||
* @example
|
||||
* s("sine").note("F1*2").release(1)
|
||||
* .penv(24).pdecay(0.05)
|
||||
* .distort(rand.range(1, 8))
|
||||
* .distorttype("<fold chebyshev scurve diode asym sinefold>")
|
||||
*/
|
||||
export const { distorttype } = registerControl('distorttype', 'disttype');
|
||||
|
||||
/**
|
||||
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
|
||||
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
|
||||
|
||||
@@ -14,7 +14,7 @@ export class Cyclist {
|
||||
onToggle,
|
||||
onError,
|
||||
getTime,
|
||||
latency = 0.1,
|
||||
latency = 0.03,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
beforeStart,
|
||||
@@ -57,7 +57,7 @@ export class Cyclist {
|
||||
}
|
||||
|
||||
// query the pattern for events
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
|
||||
|
||||
haps.forEach((hap) => {
|
||||
if (hap.hasOnset()) {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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;
|
||||
@@ -38,8 +41,7 @@ export class NeoCyclist {
|
||||
if (this.started === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' });
|
||||
haps.forEach((hap) => {
|
||||
if (hap.hasOnset()) {
|
||||
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
||||
|
||||
@@ -98,10 +98,7 @@ export class Pattern {
|
||||
|
||||
// runs func on query state
|
||||
withState(func) {
|
||||
return this.withHaps((haps, state) => {
|
||||
func(state);
|
||||
return haps;
|
||||
});
|
||||
return new Pattern((state) => this.query(func(state)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3536,3 +3533,76 @@ export const morph = (frompat, topat, bypat) => {
|
||||
bypat = reify(bypat);
|
||||
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
||||
};
|
||||
|
||||
/**
|
||||
* Soft-clipping distortion
|
||||
*
|
||||
* @name soft
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Hard-clipping distortion
|
||||
*
|
||||
* @name hard
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Cubic polynomial distortion
|
||||
*
|
||||
* @name cubic
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Diode-emulating distortion
|
||||
*
|
||||
* @name diode
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Asymmetrical diode distortion
|
||||
*
|
||||
* @name asym
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Wavefolding distortion
|
||||
*
|
||||
* @name fold
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Wavefolding distortion composed with sinusoid
|
||||
*
|
||||
* @name sinefold
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Distortion via Chebyshev polynomials
|
||||
*
|
||||
* @name chebyshev
|
||||
* @param {number | Pattern} distortion amount of distortion to apply
|
||||
* @param {number | Pattern} volume linear postgain of the distortion
|
||||
*
|
||||
*/
|
||||
const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev'];
|
||||
for (const name of distAlgoNames) {
|
||||
// Add aliases for distortion algorithms
|
||||
Pattern.prototype[name] = function (args) {
|
||||
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
|
||||
return this.distort(argsPat);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -214,7 +214,10 @@ export function repl({
|
||||
}
|
||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||
if (Object.keys(pPatterns).length) {
|
||||
let patterns = Object.values(pPatterns);
|
||||
let patterns = [];
|
||||
for (const [key, value] of Object.entries(pPatterns)) {
|
||||
patterns.push(value.withState((state) => state.setControls({ id: key })));
|
||||
}
|
||||
if (eachTransform) {
|
||||
// Explicit lambda so only element (not index and array) are passed
|
||||
patterns = patterns.map((x) => eachTransform(x));
|
||||
@@ -228,6 +231,7 @@ export function repl({
|
||||
pattern = allTransforms[i](pattern);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPattern(pattern)) {
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
|
||||
@@ -19,9 +19,9 @@ export class State {
|
||||
return this.setSpan(func(this.span));
|
||||
}
|
||||
|
||||
// Returns new State with different controls
|
||||
// Returns new State with added controls.
|
||||
setControls(controls) {
|
||||
return new State(this.span, controls);
|
||||
return new State(this.span, { ...this.controls, ...controls });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ Pattern.prototype.mqtt = function (
|
||||
cx.connect(props);
|
||||
}
|
||||
return this.withHap((hap) => {
|
||||
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
const onTrigger = (hap, currentTime, cps, targetTime) => {
|
||||
let msg_topic = topic;
|
||||
if (!cx || !cx.isConnected()) {
|
||||
return;
|
||||
|
||||
@@ -20,10 +20,13 @@ export async function prebake() {
|
||||
// import('@strudel/osc'),
|
||||
);
|
||||
// load samples
|
||||
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
|
||||
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main';
|
||||
|
||||
// TODO: move this onto the strudel repo
|
||||
const ts = 'https://raw.githubusercontent.com/todepond/samples/main/';
|
||||
const ts = 'https://raw.githubusercontent.com/todepond/samples/main';
|
||||
|
||||
const tc = 'https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main';
|
||||
|
||||
await Promise.all([
|
||||
modulesLoading,
|
||||
registerSynthSounds(),
|
||||
@@ -36,9 +39,9 @@ export async function prebake() {
|
||||
samples(`${ds}/tidal-drum-machines.json`),
|
||||
samples(`${ds}/piano.json`),
|
||||
samples(`${ds}/Dirt-Samples.json`),
|
||||
samples(`${ds}/uzu-drumkit.json`),
|
||||
samples(`${ds}/vcsl.json`),
|
||||
samples(`${ds}/mridangam.json`),
|
||||
samples(`${tc}/strudel.json`),
|
||||
]);
|
||||
|
||||
aliasBank(`${ts}/tidal-drum-machines-alias.json`);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
let audioContext;
|
||||
|
||||
export const setDefaultAudioContext = () => {
|
||||
audioContext = new AudioContext();
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
export const getAudioContext = () => {
|
||||
if (!audioContext) {
|
||||
return setDefaultAudioContext();
|
||||
}
|
||||
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
export function getAudioContextCurrentTime() {
|
||||
return getAudioContext().currentTime;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
|
||||
let worklet;
|
||||
export async function dspWorklet(ac, code) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { getNoiseBuffer } from './noise.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||
|
||||
@@ -381,6 +382,102 @@ export function applyFM(param, value, begin) {
|
||||
return { stop };
|
||||
}
|
||||
|
||||
// Saturation curves
|
||||
|
||||
const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1)
|
||||
const _mod = (n, m) => ((n % m) + m) % m;
|
||||
|
||||
const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x));
|
||||
const _soft = (x, k) => Math.tanh(x * (1 + k));
|
||||
const _hard = (x, k) => clamp((1 + k) * x, -1, 1);
|
||||
|
||||
const _fold = (x, k) => {
|
||||
// Closed form folding for audio rate
|
||||
let y = (1 + 0.5 * k) * x;
|
||||
const window = _mod(y + 1, 4);
|
||||
return 1 - Math.abs(window - 2);
|
||||
};
|
||||
|
||||
const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k));
|
||||
|
||||
const _cubic = (x, k) => {
|
||||
const t = __squash(Math.log1p(k));
|
||||
const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1)
|
||||
return _soft(cubic, k);
|
||||
};
|
||||
|
||||
const _diode = (x, k, asym = false) => {
|
||||
const g = 1 + 2 * k; // gain
|
||||
const t = __squash(Math.log1p(k));
|
||||
const bias = 0.07 * t;
|
||||
const pos = _soft(x + bias, 2 * k);
|
||||
const neg = _soft(asym ? bias : -x + bias, 2 * k);
|
||||
const y = pos - neg;
|
||||
// We divide by the derivative at 0 so that the distortion is roughly
|
||||
// the identity map near 0 => small values are preserved and undistorted
|
||||
const sech = 1 / Math.cosh(g * bias);
|
||||
const sech2 = sech * sech; // derivative of soft (i.e. tanh) is sech^2
|
||||
const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x
|
||||
return _soft(y / denom, k);
|
||||
};
|
||||
|
||||
const _asym = (x, k) => _diode(x, k, true);
|
||||
|
||||
const _chebyshev = (x, k) => {
|
||||
const kl = 10 * Math.log1p(k);
|
||||
let tnm1 = 1;
|
||||
let tnm2 = x;
|
||||
let tn;
|
||||
let y = 0;
|
||||
for (let i = 1; i < 64; i++) {
|
||||
if (i < 2) {
|
||||
// Already set inital conditions
|
||||
y += i == 0 ? tnm1 : tnm2;
|
||||
continue;
|
||||
}
|
||||
tn = 2 * x * tnm1 - tnm2; // https://en.wikipedia.org/wiki/Chebyshev_polynomials#Recurrence_definition
|
||||
tnm2 = tnm1;
|
||||
tnm1 = tn;
|
||||
if (i % 2 === 0) {
|
||||
y += Math.min((1.3 * kl) / i, 2) * tn;
|
||||
}
|
||||
}
|
||||
// Soft clip
|
||||
return _soft(y, kl / 20);
|
||||
};
|
||||
|
||||
export const distortionAlgorithms = {
|
||||
scurve: _scurve,
|
||||
soft: _soft,
|
||||
hard: _hard,
|
||||
cubic: _cubic,
|
||||
diode: _diode,
|
||||
asym: _asym,
|
||||
fold: _fold,
|
||||
sinefold: _sineFold,
|
||||
chebyshev: _chebyshev,
|
||||
};
|
||||
const _algoNames = Object.freeze(Object.keys(distortionAlgorithms));
|
||||
|
||||
export const getDistortionAlgorithm = (algo) => {
|
||||
let index = algo;
|
||||
if (typeof algo === 'string') {
|
||||
index = _algoNames.indexOf(algo);
|
||||
if (index === -1) {
|
||||
logger(`[superdough] Could not find waveshaping algorithm ${algo}.
|
||||
Available options are ${_algoNames.join(', ')}.
|
||||
Defaulting to ${_algoNames[0]}.`);
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
const name = _algoNames[index % _algoNames.length]; // allow for wrapping if algo was a number
|
||||
return distortionAlgorithms[name];
|
||||
};
|
||||
|
||||
export const getDistortion = (distort, postgain, algorithm) => {
|
||||
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } });
|
||||
};
|
||||
|
||||
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||
let { note, freq } = value;
|
||||
note = note || defaultNote;
|
||||
|
||||
@@ -11,4 +11,5 @@ export * from './synth.mjs';
|
||||
export * from './zzfx.mjs';
|
||||
export * from './logger.mjs';
|
||||
export * from './dspworklet.mjs';
|
||||
export * from './audioContext.mjs';
|
||||
export * from './wavetable.mjs';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { drywet } from './helpers.mjs';
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
|
||||
let noiseCache = {};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs';
|
||||
import { getAudioContext, registerSound, registerWaveTable } from './index.mjs';
|
||||
import { getCommonSampleInfo } from './util.mjs';
|
||||
import { registerSound, registerWaveTable } from './index.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
|
||||
@@ -9,10 +9,11 @@ import './reverb.mjs';
|
||||
import './vowel.mjs';
|
||||
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?audioworklet';
|
||||
import { createFilter, gainNode, getCompressor, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
||||
import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||
|
||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||
@@ -154,6 +155,7 @@ let defaultDefaultValues = {
|
||||
phaserdepth: 0.75,
|
||||
shapevol: 1,
|
||||
distortvol: 1,
|
||||
distorttype: 0,
|
||||
delay: 0,
|
||||
byteBeatExpression: '0',
|
||||
delayfeedback: 0.5,
|
||||
@@ -200,25 +202,6 @@ export function setVersionDefaults(version) {
|
||||
|
||||
export const resetLoadedSounds = () => soundMap.set({});
|
||||
|
||||
let audioContext;
|
||||
|
||||
export const setDefaultAudioContext = () => {
|
||||
audioContext = new AudioContext({ latencyHint: 'playback' });
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
export const getAudioContext = () => {
|
||||
if (!audioContext) {
|
||||
return setDefaultAudioContext();
|
||||
}
|
||||
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
export function getAudioContextCurrentTime() {
|
||||
return getAudioContext().currentTime;
|
||||
}
|
||||
|
||||
let externalWorklets = [];
|
||||
export function registerWorklet(url) {
|
||||
externalWorklets.push(url);
|
||||
@@ -473,6 +456,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
shapevol = getDefaultValue('shapevol'),
|
||||
distort,
|
||||
distortvol = getDefaultValue('distortvol'),
|
||||
distorttype = getDefaultValue('distorttype'),
|
||||
pan,
|
||||
vowel,
|
||||
delay = getDefaultValue('delay'),
|
||||
@@ -646,8 +630,7 @@ 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(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
|
||||
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
||||
|
||||
if (tremolosync != null) {
|
||||
tremolo = cps * tremolosync;
|
||||
|
||||
@@ -135,12 +135,14 @@ export class SuperdoughOutput {
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.disconnect();
|
||||
this.initializeAudio();
|
||||
}
|
||||
disconnect() {
|
||||
this.channelMerger.disconnect();
|
||||
this.destinationGain.disconnect();
|
||||
this.destinationGain = null;
|
||||
this.channelMerger = null;
|
||||
this.nodes = {};
|
||||
this.initializeAudio();
|
||||
}
|
||||
connectToDestination = (input, channels = [0, 1]) => {
|
||||
//This upmix can be removed if correct channel counts are set throughout the app,
|
||||
@@ -172,6 +174,7 @@ export class SuperdoughAudioController {
|
||||
Array.from(this.nodes).forEach((node) => {
|
||||
node.disconnect();
|
||||
});
|
||||
this.nodes = {};
|
||||
this.output.reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { clamp } from './util.mjs';
|
||||
import { registerSound, getAudioContext, soundMap } from './superdough.mjs';
|
||||
import { registerSound, soundMap } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
destroyAudioWorkletNode,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { getCommonSampleInfo } from './util.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
applyParameterModulators,
|
||||
destroyAudioWorkletNode,
|
||||
getADSRValues,
|
||||
getFrequencyFromValue,
|
||||
getLfo,
|
||||
getParamADSR,
|
||||
getPitchEnvelope,
|
||||
getVibratoOscillator,
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
const WT_MAX_MIP_LEVELS = 6;
|
||||
export const Warpmode = Object.freeze({
|
||||
NONE: 0,
|
||||
ASYM: 1,
|
||||
@@ -40,39 +39,25 @@ export const Warpmode = Object.freeze({
|
||||
FLIP: 21,
|
||||
});
|
||||
|
||||
async function loadWavetableFrames(url, label, frameLen = 2048) {
|
||||
const buf = await loadBuffer(url, label);
|
||||
const ch0 = buf.getChannelData(0);
|
||||
const total = ch0.length;
|
||||
const numFrames = Math.max(1, Math.floor(total / frameLen));
|
||||
const frames = new Array(numFrames);
|
||||
for (let i = 0; i < numFrames; i++) {
|
||||
const start = i * frameLen;
|
||||
frames[i] = ch0.subarray(start, start + frameLen);
|
||||
const seenKeys = new Set();
|
||||
async function getPayload(url, label, frameLen = 2048) {
|
||||
const key = `${url},${frameLen}`;
|
||||
if (!seenKeys.has(key)) {
|
||||
const buf = await loadBuffer(url, label);
|
||||
const ch0 = buf.getChannelData(0);
|
||||
const total = ch0.length;
|
||||
const numFrames = Math.max(1, Math.floor(total / frameLen));
|
||||
const frames = new Array(numFrames);
|
||||
for (let i = 0; i < numFrames; i++) {
|
||||
const start = i * frameLen;
|
||||
frames[i] = ch0.subarray(start, start + frameLen);
|
||||
}
|
||||
seenKeys.add(key);
|
||||
return { frames, frameLen, numFrames, key };
|
||||
}
|
||||
|
||||
// build mipmaps
|
||||
const mipmaps = [frames];
|
||||
let levelFrames = frames;
|
||||
for (let level = 1; level < WT_MAX_MIP_LEVELS; level++) {
|
||||
const prevLen = levelFrames[0].length;
|
||||
if (prevLen <= 32) break;
|
||||
const nextLen = prevLen >> 1;
|
||||
const next = levelFrames.map((src) => {
|
||||
const out = new Float32Array(nextLen);
|
||||
for (let j = 0; j < nextLen; j++) {
|
||||
out[j] = (src[2 * j] + src[2 * j + 1]) / 2;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
mipmaps.push(next);
|
||||
levelFrames = next;
|
||||
}
|
||||
return { frames, mipmaps, frameLen, numFrames };
|
||||
return { frameLen, key }; // worklet will use the cached version
|
||||
}
|
||||
|
||||
const loadCache = {};
|
||||
|
||||
function humanFileSize(bytes, si) {
|
||||
var thresh = si ? 1000 : 1024;
|
||||
if (bytes < thresh) return bytes + ' B';
|
||||
@@ -117,6 +102,7 @@ async function decodeAtNativeRate(arr) {
|
||||
return await tempAC.decodeAudioData(arr);
|
||||
}
|
||||
|
||||
const loadCache = {};
|
||||
const loadBuffer = (url, label) => {
|
||||
url = url.replace('#', '%23');
|
||||
if (!loadCache[url]) {
|
||||
@@ -222,7 +208,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
|
||||
};
|
||||
|
||||
export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
const { s, n = 0, duration } = value;
|
||||
const { s, n = 0, duration, clip } = value;
|
||||
const ac = getAudioContext();
|
||||
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||
let { warpmode } = value;
|
||||
@@ -231,8 +217,11 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
}
|
||||
const frequency = getFrequencyFromValue(value);
|
||||
const { url, label } = getCommonSampleInfo(value, tables);
|
||||
const payload = await loadWavetableFrames(url, label, frameLen);
|
||||
const holdEnd = t + duration;
|
||||
const payload = await getPayload(url, label, frameLen);
|
||||
let holdEnd = t + duration;
|
||||
if (clip !== undefined) {
|
||||
holdEnd = Math.min(t + clip * duration, holdEnd);
|
||||
}
|
||||
const endWithRelease = holdEnd + release;
|
||||
const envEnd = endWithRelease + 0.01;
|
||||
const source = getWorklet(
|
||||
@@ -252,7 +241,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
},
|
||||
{ outputChannelCount: [2] },
|
||||
);
|
||||
source.port.postMessage({ type: 'tables', payload });
|
||||
source.port.postMessage({ type: 'table', payload });
|
||||
if (ac.currentTime > t) {
|
||||
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||
return;
|
||||
@@ -320,17 +309,18 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
},
|
||||
);
|
||||
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
||||
const fm = applyFM(source.parameters.get('frequency'), value, t);
|
||||
const envGain = ac.createGain();
|
||||
const node = source.connect(envGain);
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
||||
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
|
||||
const handle = { node, source };
|
||||
const timeoutNode = webAudioTimeout(
|
||||
ac,
|
||||
() => {
|
||||
source.disconnect();
|
||||
destroyAudioWorkletNode(source);
|
||||
vibratoOscillator?.stop();
|
||||
fm?.stop();
|
||||
node.disconnect();
|
||||
wtPosModulators?.disconnect();
|
||||
wtWarpModulators?.disconnect();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import OLAProcessor from './ola-processor';
|
||||
import FFT from './fft.js';
|
||||
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||
|
||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
const mod = (n, m) => ((n % m) + m) % m;
|
||||
@@ -137,7 +138,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
process(_inputs, outputs, parameters) {
|
||||
const begin = parameters['begin'][0];
|
||||
if (currentTime >= parameters.end[0]) {
|
||||
return false;
|
||||
@@ -430,9 +431,10 @@ class DistortProcessor extends AudioWorkletProcessor {
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
constructor({ processorOptions }) {
|
||||
super();
|
||||
this.started = false;
|
||||
this.algorithm = getDistortionAlgorithm(processorOptions.algorithm);
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
@@ -444,13 +446,12 @@ class DistortProcessor extends AudioWorkletProcessor {
|
||||
return false;
|
||||
}
|
||||
this.started = hasInput;
|
||||
|
||||
const shape = Math.expm1(parameters.distort[0]);
|
||||
const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0]));
|
||||
|
||||
for (let n = 0; n < blockSize; n++) {
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain;
|
||||
const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
|
||||
const shape = Math.expm1(pv(parameters.distort, n));
|
||||
for (let ch = 0; ch < input.length; ch++) {
|
||||
const x = input[ch][n];
|
||||
output[ch][n] = postgain * this.algorithm(x, shape);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -510,7 +511,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
},
|
||||
];
|
||||
}
|
||||
process(input, outputs, params) {
|
||||
process(_input, outputs, params) {
|
||||
if (currentTime <= params.begin[0]) {
|
||||
return true;
|
||||
}
|
||||
@@ -1043,6 +1044,7 @@ function brownian(x, oct = 4) {
|
||||
return (sum / norm) * 2 - 1;
|
||||
}
|
||||
|
||||
const tablesCache = {};
|
||||
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
@@ -1061,31 +1063,40 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.tables = null;
|
||||
this.frameLen = 0;
|
||||
this.numFrames = 0;
|
||||
this.phase = [];
|
||||
this.syncRatio = 1;
|
||||
this.invSR = 1 / sampleRate;
|
||||
|
||||
this.port.onmessage = (e) => {
|
||||
const { type, payload } = e.data || {};
|
||||
if (type === 'tables') {
|
||||
this.tables = payload.mipmaps;
|
||||
if (type === 'table') {
|
||||
const key = payload.key;
|
||||
this.frameLen = payload.frameLen;
|
||||
if (!tablesCache[key]) {
|
||||
const tables = [payload.frames];
|
||||
let table = tables[0];
|
||||
for (let level = 1; level < 1; level++) {
|
||||
const nextLen = table.length >> 1;
|
||||
const nextTable = table.map((frame) => {
|
||||
const avg = new Float32Array(nextLen);
|
||||
for (let i = 0; i < nextLen; i++) {
|
||||
avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2;
|
||||
}
|
||||
return avg;
|
||||
});
|
||||
tables.push(nextTable);
|
||||
table = nextTable;
|
||||
if (nextLen <= 32) break;
|
||||
}
|
||||
tablesCache[key] = tables;
|
||||
}
|
||||
this.tables = tablesCache[key];
|
||||
this.numFrames = this.tables[0].length;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_chooseMip(dphi) {
|
||||
const approxHarm = Math.min(64, 1 / Math.max(1e-6, dphi));
|
||||
let level = 0;
|
||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
||||
level++;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
_mirror(x) {
|
||||
return 1 - Math.abs(2 * x - 1);
|
||||
}
|
||||
@@ -1222,14 +1233,25 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
_sampleFrame(frame, phase) {
|
||||
const pos = phase * frame.length;
|
||||
const len = frame.length;
|
||||
const pos = phase * len;
|
||||
const i = pos | 0;
|
||||
const frac = pos - i;
|
||||
const a = frame[i % frame.length];
|
||||
const b = frame[(i + 1) % frame.length];
|
||||
const a = frame[i];
|
||||
const i1 = i + 1 < len ? i + 1 : 0; // fast wrap
|
||||
const b = frame[i1];
|
||||
return a + (b - a) * frac;
|
||||
}
|
||||
|
||||
_chooseMip(dphi) {
|
||||
const approxHarm = clamp(dphi, 1e-6, 64);
|
||||
let level = 0;
|
||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
||||
level++;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
process(_inputs, outputs, parameters) {
|
||||
if (currentTime >= parameters.end[0]) {
|
||||
return false;
|
||||
@@ -1239,29 +1261,27 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
const outL = outputs[0][0];
|
||||
const outR = outputs[0][1] || outputs[0][0];
|
||||
|
||||
if (!this.tables) {
|
||||
outL.fill(0);
|
||||
if (outR !== outL) outR.set(outL);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < outL.length; i++) {
|
||||
const detune = pv(parameters.detune, i);
|
||||
const tablePos = pv(parameters.position, i);
|
||||
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||
const idx = tablePos * (this.numFrames - 1);
|
||||
const fIdx = idx | 0;
|
||||
const frac = idx - fIdx;
|
||||
const warpAmount = pv(parameters.warp, i);
|
||||
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
||||
const warpMode = pv(parameters.warpMode, i);
|
||||
const voices = pv(parameters.voices, i);
|
||||
const phaseRand = pv(parameters.phaserand, i);
|
||||
const spread = voices > 1 ? pv(parameters.spread, i) : 0;
|
||||
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);
|
||||
let f = pv(parameters.frequency, i);
|
||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||
const normalizer = 0.3 / Math.sqrt(voices);
|
||||
const normalizer = 1 / Math.sqrt(voices);
|
||||
for (let n = 0; n < voices; n++) {
|
||||
const isOdd = (n & 1) == 1;
|
||||
let gainL = gain1;
|
||||
@@ -1272,7 +1292,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
gainR = gain1;
|
||||
}
|
||||
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
|
||||
const dPhase = fVoice / sampleRate;
|
||||
const dPhase = fVoice * this.invSR;
|
||||
const level = this._chooseMip(dPhase);
|
||||
const table = this.tables[level];
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//import { ZZFX } from 'zzfx';
|
||||
import { midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { registerSound, getAudioContext } from './superdough.mjs';
|
||||
import { registerSound } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
import { buildSamples } from './zzfx_fork.mjs';
|
||||
|
||||
export const getZZFX = (value, t) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
import { getAudioContext } from './audioContext.mjs';
|
||||
|
||||
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
|
||||
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
|
||||
|
||||
@@ -2829,6 +2829,82 @@ exports[`runs examples > example "distort" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "distort" example index 2 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 1/4 → 1/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 1/2 → 3/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 3/4 → 1/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 1/1 → 5/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 5/4 → 3/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 3/2 → 7/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 7/4 → 2/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 2/1 → 9/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 9/4 → 5/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 5/2 → 11/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 11/4 → 3/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 3/1 → 13/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 13/4 → 7/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 7/2 → 15/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
"[ 15/4 → 4/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "distorttype" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distorttype:1 ]",
|
||||
"[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distorttype:1 ]",
|
||||
"[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distorttype:1 ]",
|
||||
"[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distorttype:1 ]",
|
||||
"[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distorttype:2 ]",
|
||||
"[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distorttype:2 ]",
|
||||
"[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distorttype:2 ]",
|
||||
"[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distorttype:2 ]",
|
||||
"[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
"[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distorttype:0 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "distorttype" example index 1 1`] = `
|
||||
[
|
||||
"[ (0/1 → 1/2) ⇝ 1/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]",
|
||||
"[ 0/1 ⇜ (1/2 → 1/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]",
|
||||
"[ (1/1 → 3/2) ⇝ 2/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]",
|
||||
"[ 1/1 ⇜ (3/2 → 2/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]",
|
||||
"[ (2/1 → 5/2) ⇝ 3/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]",
|
||||
"[ 2/1 ⇜ (5/2 → 3/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]",
|
||||
"[ (3/1 → 7/2) ⇝ 4/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]",
|
||||
"[ 3/1 ⇜ (7/2 → 4/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "distortvol" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
"[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "djf" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]",
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -7,6 +7,21 @@ layout: ../../layouts/MainLayout.astro
|
||||
|
||||
This Guide shows you the different ways to get started with using Strudel in your own project.
|
||||
|
||||
## Respect the license
|
||||
|
||||
First, please take a moment to understand Strudel's free/open source license,
|
||||
[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.en.html).
|
||||
|
||||
Here is a lay summary, but check the license for legal definitions and responsibilities.
|
||||
|
||||
- You can distribute modified versions if you keep track of the changes and the date you made them.
|
||||
- You must license derivative work under the same license.
|
||||
- Source code must be distributed along with web publication.
|
||||
|
||||
Among other things, it means that when you share your work, the whole application must be shared under the same free/open source license, or one compatible with it. This is because we want Strudel to stay free/open source. In other words, you are not permitted to distribute integrations of Strudel with libraries or other code that does not have a compatible free/open source license.
|
||||
|
||||
This also applies to clones informed by reading Strudel's source code, as legally speaking, that counts as a 'derivative work'. Again, please [read the licence](https://www.gnu.org/licenses/agpl-3.0.en.html) for details.
|
||||
|
||||
## Embedding the Strudel REPL
|
||||
|
||||
There are 3 quick ways to embed strudel in your website:
|
||||
|
||||
@@ -44,10 +44,10 @@ export function Reference() {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lowCaseSearch = search.toLowerCase();
|
||||
const lowerCaseSearch = search.toLowerCase();
|
||||
return (
|
||||
entry.name.toLowerCase().includes(lowCaseSearch) ||
|
||||
(entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false)
|
||||
entry.name.toLowerCase().includes(lowerCaseSearch) ||
|
||||
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
|
||||
);
|
||||
});
|
||||
}, [search]);
|
||||
|
||||
Reference in New Issue
Block a user