Compare commits

..

1 Commits

Author SHA1 Message Date
Alex McLean 15c40d2b37 widget to print values 2025-10-14 11:51:06 +01:00
11 changed files with 112 additions and 161 deletions
+6
View File
@@ -140,3 +140,9 @@ registerWidget('_spectrum', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d'); const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.spectrum({ ...options, ctx, id }); return pat.spectrum({ ...options, ctx, id });
}); });
registerWidget('_print', (id, options = {}, pat) => {
options = { width: 500, height: 30, ...options };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.print({ ...options, ctx, id });
});
+1
View File
@@ -4,3 +4,4 @@ export * from './draw.mjs';
export * from './pianoroll.mjs'; export * from './pianoroll.mjs';
export * from './spiral.mjs'; export * from './spiral.mjs';
export * from './pitchwheel.mjs'; export * from './pitchwheel.mjs';
export * from './print.mjs';
+50
View File
@@ -0,0 +1,50 @@
import { getTheme, getDrawContext } from './draw.mjs';
import { Pattern } from '@strudel/core';
export function print({ haps, ctx, id, margin = 10, fontsize = 24 } = {}) {
const w = ctx.canvas.width;
const h = ctx.canvas.height;
ctx.clearRect(0, 0, w, h);
const color = getTheme().foreground;
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.globalAlpha = 1;
ctx.textAlign = 'left';
haps.forEach((hap) => {
if (hap.hasOnset()) {
const hapColor = hap.value.color || color;
ctx.strokeStyle = hapColor;
ctx.fillStyle = hapColor;
const { velocity = 1, gain = 1 } = hap.value || {};
const alpha = velocity * gain;
ctx.globalAlpha = alpha;
ctx.font = `${fontsize}px sans-serif`;
ctx.fillText(hap.value, 0, fontsize);
}
});
return;
}
Pattern.prototype.print = function (options = {}) {
let { ctx = getDrawContext(), id = 1 } = options;
this.draw(
(haps, time) => {
print({
...options,
time,
ctx,
haps: haps.filter((hap) => hap.isActive(time)),
});
},
{
lookbehind: 0,
lookahead: 0,
id,
},
);
return this;
};
-1
View File
@@ -153,7 +153,6 @@ samples('github:tidalcycles/dirt-samples')
The format is `github:<user>/<repo>/<branch>`. 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. 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. The format is also expected to be the same as explained above.
+22 -35
View File
@@ -1,4 +1,4 @@
import { getCommonSampleInfoFromBank } from './util.mjs'; import { getCommonSampleInfo } from './util.mjs';
import { registerSound, registerWaveTable } from './index.mjs'; import { registerSound, registerWaveTable } from './index.mjs';
import { getAudioContext } from './audioContext.mjs'; import { getAudioContext } from './audioContext.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
@@ -23,34 +23,35 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u]; 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 };
}
export const getSampleBuffer = async (label, sampleUrl, resolveUrl) => { // takes hapValue and returns buffer + playbackRate.
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
if (resolveUrl) { if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl); sampleUrl = await resolveUrl(sampleUrl);
} }
const ac = getAudioContext(); const ac = getAudioContext();
const buffer = await loadBuffer(sampleUrl, ac, label); 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') { if (hapValue.unit === 'c') {
playbackRate = playbackRate * buffer.duration; playbackRate = playbackRate * buffer.duration;
} }
return playbackRate; return { buffer, playbackRate };
} };
// creates playback ready AudioBufferSourceNode from hapValue // creates playback ready AudioBufferSourceNode from hapValue
export function getSampleBufferSource(hapValue, buffer,transpose) { export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl);
if (hapValue.speed < 0) { if (hapValue.speed < 0) {
// should this be cached? // should this be cached?
buffer = reverseBuffer(buffer); buffer = reverseBuffer(buffer);
} }
const playbackRate = getBufferPlaybackRate(hapValue, buffer, transpose)
const ac = getAudioContext(); const ac = getAudioContext();
const bufferSource = ac.createBufferSource(); const bufferSource = ac.createBufferSource();
bufferSource.buffer = buffer; bufferSource.buffer = buffer;
@@ -120,20 +121,13 @@ function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) { if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl'); throw new Error('expected "github:" at the start of pseudoUrl');
} }
let path = base.slice('github:'.length); let [_, path] = base.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path; path = path.endsWith('/') ? path.slice(0, -1) : path;
if (path.split('/').length === 2) {
let components = path.split('/'); // assume main as default branch if none set
let user = components[0]; path += '/main';
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);
} }
other = other.join('/'); return `https://raw.githubusercontent.com/${path}/${subpath}`;
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
} }
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => { export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
@@ -263,7 +257,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
const cutGroups = []; const cutGroups = [];
export async function onTriggerSample(t, value, onended, bufferSrc) { export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
let { let {
s, s,
nudge = 0, // TODO: is this in seconds? nudge = 0, // TODO: is this in seconds?
@@ -285,7 +279,7 @@ export async function onTriggerSample(t, value, onended, bufferSrc) {
// destructure adsr here, because the default should be different for synths and samples // 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]); let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
const { bufferSource, sliceDuration, offset } = bufferSrc const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
// asny stuff above took too long? // asny stuff above took too long?
if (ac.currentTime > t) { if (ac.currentTime > t) {
@@ -347,21 +341,14 @@ export async function onTriggerSample(t, value, onended, bufferSrc) {
return handle; 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) { function registerSample(key, bank, params) {
registerSound(key, async (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, await getBufferSrcFromBank(hapValue, bank, undefined)), { registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
type: 'sample', type: 'sample',
samples: bank, samples: bank,
...params, ...params,
}); });
} }
export function registerSampleSource(key, bank, params) { export function registerSampleSource(key, bank, params) {
const isWavetable = key.startsWith('wt_'); const isWavetable = key.startsWith('wt_');
if (isWavetable) { if (isWavetable) {
+4 -19
View File
@@ -80,13 +80,12 @@ export function secondsToCycle(t, cps) {
// deduces relevant info for sample loading from hap.value and sample definition // deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function // 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) // hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getCommonSampleInfoFromBank(hapValue, bank) { export function getCommonSampleInfo(hapValue, bank) {
const { s, n = 0 } = hapValue; const { s, n = 0 } = hapValue;
let midi = valueToMidi(hapValue, 36); let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let url; let url;
let index = 0; let index = 0;
let {transpose, label} = getCommonSampleInfo(hapValue)
if (Array.isArray(bank)) { if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length); index = getSoundIndex(n, bank.length);
url = bank[index]; url = bank[index];
@@ -103,20 +102,6 @@ export function getCommonSampleInfoFromBank(hapValue, bank) {
index = getSoundIndex(n, bank[closest].length); index = getSoundIndex(n, bank[closest].length);
url = bank[closest][index]; url = bank[closest][index];
} }
label = `${s}:${index}`; const label = `${s}:${index}`;
return { transpose, url, label }; return { transpose, url, index, midi, 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 { getAudioContext, registerSound } from './index.mjs';
import { getCommonSampleInfoFromBank } from './util.mjs'; import { getCommonSampleInfo } from './util.mjs';
import { import {
applyFM, applyFM,
applyParameterModulators, applyParameterModulators,
@@ -216,7 +216,7 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE; warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE;
} }
const frequency = getFrequencyFromValue(value); const frequency = getFrequencyFromValue(value);
const { url, label } = getCommonSampleInfoFromBank(value, tables); const { url, label } = getCommonSampleInfo(value, tables);
const payload = await getPayload(url, label, frameLen); const payload = await getPayload(url, label, frameLen);
let holdEnd = t + duration; let holdEnd = t + duration;
if (clip !== undefined) { if (clip !== undefined) {
@@ -231,12 +231,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
begin: t, begin: t,
end: envEnd, end: envEnd,
frequency, frequency,
freqspread: value.detune, detune: value.detune,
position: value.wt, position: value.wt,
warp: value.warp, warp: value.warp,
warpMode: warpmode, warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1), voices: Math.max(value.unison ?? 1, 1),
panspread: value.spread, spread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0, phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
}, },
{ outputChannelCount: [2] }, { outputChannelCount: [2] },
+13 -17
View File
@@ -1050,15 +1050,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
return [ return [
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, { name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, { name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON }, { name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 },
{ name: 'detune', defaultValue: 0 }, { name: 'detune', defaultValue: 0.18 },
{ name: 'freqspread', defaultValue: 0.18, min: 0 }, { name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
{ name: 'position', defaultValue: 0, min: 0, max: 1 }, { name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
{ name: 'warpMode', defaultValue: 0 }, { name: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, min: 1 }, { name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 }, { name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 },
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 }, { name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 },
]; ];
} }
@@ -1236,12 +1235,10 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
_sampleFrame(frame, phase) { _sampleFrame(frame, phase) {
const len = frame.length; const len = frame.length;
const pos = phase * len; const pos = phase * len;
let i = pos | 0; const i = pos | 0;
if (i >= len) i = 0; // fast wrap
const frac = pos - i; const frac = pos - i;
const a = frame[i]; const a = frame[i];
let i1 = i + 1; const i1 = i + 1 < len ? i + 1 : 0; // fast wrap
if (i1 >= len) i1 = 0;
const b = frame[i1]; const b = frame[i1];
return a + (b - a) * frac; return a + (b - a) * frac;
} }
@@ -1271,7 +1268,6 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
} }
for (let i = 0; i < outL.length; i++) { for (let i = 0; i < outL.length; i++) {
const detune = pv(parameters.detune, i); const detune = pv(parameters.detune, i);
const freqspread = pv(parameters.freqspread, i);
const tablePos = clamp(pv(parameters.position, i), 0, 1); const tablePos = clamp(pv(parameters.position, i), 0, 1);
const idx = tablePos * (this.numFrames - 1); const idx = tablePos * (this.numFrames - 1);
const fIdx = idx | 0; const fIdx = idx | 0;
@@ -1280,9 +1276,9 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
const warpMode = pv(parameters.warpMode, i); const warpMode = pv(parameters.warpMode, i);
const voices = pv(parameters.voices, i); const voices = pv(parameters.voices, i);
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0; const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0;
const gain1 = Math.sqrt(0.5 - 0.5 * panspread); const gain1 = Math.sqrt(0.5 - 0.5 * spread);
const gain2 = Math.sqrt(0.5 + 0.5 * panspread); const gain2 = Math.sqrt(0.5 + 0.5 * spread);
let f = pv(parameters.frequency, i); let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 1 / Math.sqrt(voices); const normalizer = 1 / Math.sqrt(voices);
@@ -1295,7 +1291,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
gainL = gain2; gainL = gain2;
gainR = gain1; gainR = gain1;
} }
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
const dPhase = fVoice * this.invSR; const dPhase = fVoice * this.invSR;
const level = this._chooseMip(dPhase); const level = this._chooseMip(dPhase);
const table = this.tables[level]; const table = this.tables[level];
-17
View File
@@ -60,23 +60,6 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp
.room(0.5)`} .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 # Comments
The `//` in the example above is a line comment, resulting in the `delay` function being ignored. 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 ### scrub
<JsDoc client:idle name="Pattern.scrub" h={0} /> <JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
### speed ### speed
+10 -66
View File
@@ -1,8 +1,6 @@
import { getSampleBufferSource, onTriggerSample, registerSampleSource } from '@strudel/webaudio'; import { registerSampleSource } from '@strudel/webaudio';
import { isAudioFile } from './files.mjs'; import { isAudioFile } from './files.mjs';
import { getSoundIndex, logger } from '@strudel/core'; import { logger } from '@strudel/core';
import { registerSound } from '@strudel/webaudio';
import { getCommonSampleInfo } from '../../../packages/superdough/util.mjs';
//utilites for writing and reading to the indexdb //utilites for writing and reading to the indexdb
@@ -27,15 +25,6 @@ function clearAllIDB() {
export function clearIDB(dbName) { export function clearIDB(dbName) {
return window.indexedDB.deleteDatabase(dbName); return window.indexedDB.deleteDatabase(dbName);
}
function registerSampleFromIdb(key, bank, params) {
} }
// queries the DB, and registers the sounds so they can be played // queries the DB, and registers the sounds so they can be played
@@ -63,76 +52,31 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
if (!isAudioFile(title)) { if (!isAudioFile(title)) {
return; return;
} }
const splitRelativePath = soundFile.id.split('/'); const splitRelativePath = soundFile.id.split('/');
let parentDirectory = let parentDirectory =
//fallback to file name before period and seperator if no parent directory //fallback to file name before period and seperator if no parent directory
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
const blob = soundFile.blob;
// const blob = soundFile.blob; return blobToDataUrl(blob).then((soundPath) => {
const titlePathMap = sounds.get(parentDirectory) ?? new Map(); const titlePathMap = sounds.get(parentDirectory) ?? new Map();
titlePathMap.set(title, soundFile.id); titlePathMap.set(title, soundPath);
sounds.set(parentDirectory, titlePathMap); 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(() => { .then(() => {
sounds.forEach((titlePathMap, key) => { sounds.forEach((titlePathMap, key) => {
const bank = Array.from(titlePathMap.keys()) const value = Array.from(titlePathMap.keys())
.sort((a, b) => { .sort((a, b) => {
return a.localeCompare(b); return a.localeCompare(b);
}) })
.map((title) => titlePathMap.get(title)); .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'); logger('imported sounds registered!', 'success');