mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-14 06:43:47 -04:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c8f6302b7 | |||
| 7eebf3a466 | |||
| 64f16c4f33 | |||
| ffbbc43c89 | |||
| 086596c47c | |||
| fdde05e751 | |||
| 694162a7b1 | |||
| cbcb25b063 | |||
| 0d08225983 | |||
| 00872bd1de | |||
| b7d98dd370 | |||
| 0ecacae71e | |||
| 8cd497abb2 | |||
| 070055d2ef | |||
| e500ebe268 | |||
| 3a7ec18d46 | |||
| a2b8407fbb | |||
| ac1fd88d8d | |||
| 9f8143e062 | |||
| cdc5e5f3f3 | |||
| bad498a1e5 | |||
| 4de0c11f8c | |||
| f360d394a7 |
@@ -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()) {
|
||||
|
||||
@@ -38,8 +38,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);
|
||||
|
||||
+18
-20
@@ -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)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -966,26 +963,13 @@ export const arp = register(
|
||||
false,
|
||||
);
|
||||
|
||||
/*
|
||||
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
|
||||
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
|
||||
* @name weave
|
||||
* @memberof Pattern
|
||||
* @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
|
||||
* @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
|
||||
|
||||
addToPrototype('weave', function (t, ...pats) {
|
||||
return this.weaveWith(t, ...pats.map((x) => set.out(x)));
|
||||
});
|
||||
|
||||
*/
|
||||
/*
|
||||
* Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
|
||||
* @name weaveWith
|
||||
* @memberof Pattern
|
||||
*/
|
||||
|
||||
addToPrototype('weaveWith', function (t, ...funcs) {
|
||||
const pat = this;
|
||||
export const weaveWith = register('weaveWith', function (t, funcs, pat) {
|
||||
const l = funcs.length;
|
||||
t = Fraction(t);
|
||||
if (l == 0) {
|
||||
@@ -993,7 +977,21 @@ addToPrototype('weaveWith', function (t, ...funcs) {
|
||||
}
|
||||
return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
|
||||
});
|
||||
*/
|
||||
|
||||
/*
|
||||
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
|
||||
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
|
||||
* @name weave
|
||||
* @memberof Pattern
|
||||
* @example pan(saw).weave(4, [s("bd(3,8)"), s("~ sd")])
|
||||
* @example n("0 1 2 3 4 5 6 7").weave(8, [s("bd(3,8)"), s("~ sd")])
|
||||
*/
|
||||
export const weave = register('weave', function (t, pats, pat) {
|
||||
return pat.weaveWith(
|
||||
t,
|
||||
pats.map((x) => (y) => y.set.out(x)),
|
||||
);
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// compose matrix functions
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
destroyAudioWorkletNode,
|
||||
getADSRValues,
|
||||
getFrequencyFromValue,
|
||||
getLfo,
|
||||
getParamADSR,
|
||||
getPitchEnvelope,
|
||||
getVibratoOscillator,
|
||||
@@ -14,7 +13,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 +38,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 +101,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 +207,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 +216,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 +240,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;
|
||||
@@ -322,13 +310,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), 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();
|
||||
node.disconnect();
|
||||
|
||||
@@ -137,7 +137,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;
|
||||
@@ -510,7 +510,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
},
|
||||
];
|
||||
}
|
||||
process(input, outputs, params) {
|
||||
process(_input, outputs, params) {
|
||||
if (currentTime <= params.begin[0]) {
|
||||
return true;
|
||||
}
|
||||
@@ -1043,6 +1043,7 @@ function brownian(x, oct = 4) {
|
||||
return (sum / norm) * 2 - 1;
|
||||
}
|
||||
|
||||
const tablesCache = {};
|
||||
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
@@ -1061,31 +1062,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 +1232,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 +1260,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 +1291,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];
|
||||
|
||||
|
||||
@@ -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