mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 13:13:10 -04:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39013c09ef | |||
| 9d425dfff6 | |||
| 6e58c973af | |||
| 2473a5391d | |||
| 4e17cfbdd6 | |||
| 83c7e63432 | |||
| 909e0154fe | |||
| ef4e21ac40 | |||
| 3c1a8c8bdb | |||
| b7e941c649 | |||
| 9c5c71c31a | |||
| af53bab259 | |||
| 0065db8569 | |||
| 6f703e48e7 | |||
| 1ae773acc4 | |||
| 120f89d57f | |||
| 99e14dae5c | |||
| ac582b4d40 |
@@ -140,9 +140,3 @@ 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 });
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -4,4 +4,3 @@ 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';
|
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
@@ -153,6 +153,7 @@ 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.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getCommonSampleInfo } from './util.mjs';
|
import { getCommonSampleInfoFromBank } 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,35 +23,34 @@ 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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// takes hapValue and returns buffer + playbackRate.
|
export const getSampleBuffer = async (label, sampleUrl, resolveUrl) => {
|
||||||
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 { buffer, playbackRate };
|
return playbackRate;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// creates playback ready AudioBufferSourceNode from hapValue
|
// creates playback ready AudioBufferSourceNode from hapValue
|
||||||
export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
export function getSampleBufferSource(hapValue, buffer,transpose) {
|
||||||
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;
|
||||||
@@ -121,13 +120,20 @@ 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.split('github:');
|
let path = base.slice('github:'.length);
|
||||||
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
if (path.split('/').length === 2) {
|
|
||||||
// assume main as default branch if none set
|
let components = path.split('/');
|
||||||
path += '/main';
|
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 || '') => {
|
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
|
||||||
@@ -257,7 +263,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
|
|||||||
|
|
||||||
const cutGroups = [];
|
const cutGroups = [];
|
||||||
|
|
||||||
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
export async function onTriggerSample(t, value, onended, bufferSrc) {
|
||||||
let {
|
let {
|
||||||
s,
|
s,
|
||||||
nudge = 0, // TODO: is this in seconds?
|
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
|
// 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 } = await getSampleBufferSource(value, bank, resolveUrl);
|
const { bufferSource, sliceDuration, offset } = bufferSrc
|
||||||
|
|
||||||
// asny stuff above took too long?
|
// asny stuff above took too long?
|
||||||
if (ac.currentTime > t) {
|
if (ac.currentTime > t) {
|
||||||
@@ -341,14 +347,21 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
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, (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',
|
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) {
|
||||||
|
|||||||
@@ -80,12 +80,13 @@ 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 getCommonSampleInfo(hapValue, bank) {
|
export function getCommonSampleInfoFromBank(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];
|
||||||
@@ -102,6 +103,20 @@ export function getCommonSampleInfo(hapValue, bank) {
|
|||||||
index = getSoundIndex(n, bank[closest].length);
|
index = getSoundIndex(n, bank[closest].length);
|
||||||
url = bank[closest][index];
|
url = bank[closest][index];
|
||||||
}
|
}
|
||||||
const label = `${s}:${index}`;
|
label = `${s}:${index}`;
|
||||||
return { transpose, url, index, midi, label };
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getAudioContext, registerSound } from './index.mjs';
|
import { getAudioContext, registerSound } from './index.mjs';
|
||||||
import { getCommonSampleInfo } from './util.mjs';
|
import { getCommonSampleInfoFromBank } 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 } = getCommonSampleInfo(value, tables);
|
const { url, label } = getCommonSampleInfoFromBank(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,
|
||||||
detune: value.detune,
|
freqspread: 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),
|
||||||
spread: value.spread,
|
panspread: value.spread,
|
||||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||||
},
|
},
|
||||||
{ outputChannelCount: [2] },
|
{ outputChannelCount: [2] },
|
||||||
|
|||||||
@@ -1050,14 +1050,15 @@ 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: 220, minValue: 0.01, maxValue: 20000 },
|
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
||||||
{ name: 'detune', defaultValue: 0.18 },
|
{ name: 'detune', defaultValue: 0 },
|
||||||
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
|
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||||
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
|
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
||||||
|
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
||||||
{ name: 'warpMode', defaultValue: 0 },
|
{ name: 'warpMode', defaultValue: 0 },
|
||||||
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
|
{ name: 'voices', defaultValue: 1, min: 1 },
|
||||||
{ name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 },
|
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
||||||
{ name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 },
|
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1235,10 +1236,12 @@ 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;
|
||||||
const i = pos | 0;
|
let 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];
|
||||||
const i1 = i + 1 < len ? i + 1 : 0; // fast wrap
|
let i1 = i + 1;
|
||||||
|
if (i1 >= len) i1 = 0;
|
||||||
const b = frame[i1];
|
const b = frame[i1];
|
||||||
return a + (b - a) * frac;
|
return a + (b - a) * frac;
|
||||||
}
|
}
|
||||||
@@ -1268,6 +1271,7 @@ 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;
|
||||||
@@ -1276,9 +1280,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 spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0;
|
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
||||||
const gain1 = Math.sqrt(0.5 - 0.5 * spread);
|
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
||||||
const gain2 = Math.sqrt(0.5 + 0.5 * spread);
|
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
||||||
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);
|
||||||
@@ -1291,7 +1295,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
gainL = gain2;
|
gainL = gain2;
|
||||||
gainR = gain1;
|
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 dPhase = fVoice * this.invSR;
|
||||||
const level = this._chooseMip(dPhase);
|
const level = this._chooseMip(dPhase);
|
||||||
const table = this.tables[level];
|
const table = this.tables[level];
|
||||||
|
|||||||
@@ -60,6 +60,23 @@ 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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { registerSampleSource } from '@strudel/webaudio';
|
import { getSampleBufferSource, onTriggerSample, registerSampleSource } from '@strudel/webaudio';
|
||||||
import { isAudioFile } from './files.mjs';
|
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
|
//utilites for writing and reading to the indexdb
|
||||||
|
|
||||||
@@ -25,6 +27,15 @@ 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
|
||||||
@@ -52,31 +63,76 @@ 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, soundPath);
|
titlePathMap.set(title, soundFile.id);
|
||||||
|
|
||||||
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 value = Array.from(titlePathMap.keys())
|
const bank = 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');
|
||||||
|
|||||||
Reference in New Issue
Block a user