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
8 changed files with 161 additions and 55 deletions
+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];
+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');