From c1ce72469cd46f8332ad100411e9cfc8423e3a1e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 27 Jun 2022 00:01:28 +0200 Subject: [PATCH] webaudio optimizations - samples with obejct format for pitch declaration - support note to repitch samples - support choke to fit samples to hap duration - support "legacy" context.velocity in .out - support ":" inside s or note to set n - fix sample fadeout for soundfonts and choke - move gain before filters --- packages/core/controls.mjs | 1 + packages/soundfonts/fontloader.mjs | 3 ++ packages/webaudio/sampler.mjs | 28 +++++++--- packages/webaudio/webaudio.mjs | 84 +++++++++++++++++++++++------- 4 files changed, 91 insertions(+), 25 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index dd265d45..c9dc15b6 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -744,6 +744,7 @@ const generic_params = [ ['f', 'uid', ''], ['f', 'val', ''], ['f', 'cps', ''], + ['f', 'choke', ''], ]; // TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13 diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index d4d165df..341dfde7 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -16,6 +16,9 @@ async function loadFont(name) { } export async function getFontBufferSource(name, pitch, ac) { + if (typeof pitch === 'string') { + pitch = toMidi(pitch); + } const { buffer, zone } = await getFontPitch(name, pitch, ac); const src = ac.createBufferSource(); src.buffer = buffer; diff --git a/packages/webaudio/sampler.mjs b/packages/webaudio/sampler.mjs index 30e94169..44e51583 100644 --- a/packages/webaudio/sampler.mjs +++ b/packages/webaudio/sampler.mjs @@ -97,12 +97,28 @@ export const samples = (sampleMap, baseUrl = sampleMap._base) => { sampleCache.current = { ...sampleCache.current, ...Object.fromEntries( - Object.entries(sampleMap).map(([key, value]) => [ - key, - (typeof value === 'string' ? [value] : value).map((v) => - (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/'), - ), - ]), + Object.entries(sampleMap).map(([key, value]) => { + if (typeof value === 'string') { + return [key, [value]]; + } + if (typeof value !== 'object') { + throw new Error('wrong sample map format for ' + key); + } + baseUrl = value._base || baseUrl; + const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/'); + if (Array.isArray(value)) { + return [key, value.map(replaceUrl)]; + } + // must be object + return [ + key, + Object.fromEntries( + Object.entries(value).map(([note, samples]) => { + return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)]; + }), + ), + ]; + }), ), }; }; diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index fea9df25..b1616719 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th // import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core'; import * as strudel from '@strudel.cycles/core'; -import { fromMidi } from '@strudel.cycles/core'; +import { fromMidi, toMidi } from '@strudel.cycles/core'; import { loadBuffer } from './sampler.mjs'; const { Pattern } = strudel; @@ -68,7 +68,11 @@ const getSoundfontKey = (s) => { return; }; -const getSampleBufferSource = async (s, n) => { +const getSampleBufferSource = async (s, n, note) => { + let transpose = 0; + if (note) { + transpose = toMidi(note) - 36; // C3 is middle C + } const ac = getAudioContext(); // is sample from loaded samples(..) const samples = getLoadedSamples(); @@ -79,10 +83,33 @@ const getSampleBufferSource = async (s, n) => { if (!bank) { throw new Error('sample not found:', s, 'try one of ' + Object.keys(samples)); } - const sampleUrl = bank[n % bank.length]; + if (typeof bank !== 'object') { + throw new Error('wrong format for sample bank:', s); + } + let sampleUrl; + if (Array.isArray(bank)) { + sampleUrl = bank[n % bank.length]; + } else { + if (!note) { + throw new Error('no note(...) set for sound', s); + } + const midiDiff = (noteA) => toMidi(noteA) - toMidi(note); + // object format will expect keys as notes + const closest = Object.keys(bank) + .filter((k) => !k.startsWith('_')) + .reduce( + (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), + null, + ); + transpose = -midiDiff(closest); // semitones to repitch + sampleUrl = bank[closest][n % bank[closest].length]; + } const buffer = await loadBuffer(sampleUrl, ac); const bufferSource = ac.createBufferSource(); bufferSource.buffer = buffer; + const playbackRate = 1.0 * Math.pow(2, transpose / 12); + // bufferSource.playbackRate.value = Math.pow(2, transpose / 12); + bufferSource.playbackRate.value = playbackRate; return bufferSource; }; @@ -96,7 +123,9 @@ Pattern.prototype.out = function () { freq, s, sf, + choke = 0, // if 1, samples will be cut off when the hap ends n = 0, + note, gain = 1, cutoff, resonance = 1, @@ -106,19 +135,29 @@ Pattern.prototype.out = function () { bandq = 1, pan, attack = 0.001, - decay = 0, - sustain = 1, + decay = 0.05, + sustain = 0.5, release = 0.001, speed = 1, // sample playback speed begin = 0, end = 1, } = hap.value; + const { velocity = 1 } = hap.context; + gain *= velocity; // legacy fix for velocity // the chain will hold all audio nodes that connect to each other const chain = []; - if (typeof n === 'string') { - n = toMidi(n); // e.g. c3 => 48 + if (typeof s === 'string' && s.includes(':')) { + [s, n] = s.split(':'); + } + if (typeof note === 'string' && note.includes(':')) { + [note, n] = note.split(':'); } if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) { + // with synths, n and note are the same thing + n = note || n; + if (typeof n === 'string') { + n = toMidi(n); // e.g. c3 => 48 + } // get frequency if (!freq && typeof n === 'number') { freq = fromMidi(n); // + 48); @@ -150,10 +189,10 @@ Pattern.prototype.out = function () { try { if (soundfont) { // is soundfont - bufferSource = await globalThis.getFontBufferSource(soundfont, n, ac); + bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac); } else { // is sample from loaded samples(..) - bufferSource = await getSampleBufferSource(s, n); + bufferSource = await getSampleBufferSource(s, n, note); } } catch (err) { console.warn(err); @@ -170,27 +209,34 @@ Pattern.prototype.out = function () { } bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value; // TODO: nudge, unit, cut, loop - let duration = soundfont ? hap.duration : bufferSource.buffer.duration; + let duration = soundfont || choke ? hap.duration : bufferSource.buffer.duration; // let duration = bufferSource.buffer.duration; const offset = begin * duration; duration = ((end - begin) * duration) / Math.abs(speed); - if (soundfont) { + if (soundfont || choke) { bufferSource.start(t, offset); // duration does not work here for some reason } else { bufferSource.start(t, offset, duration); } - bufferSource.stop(t + duration); chain.push(bufferSource); - if (soundfont) { + if (soundfont || choke) { const env = ac.createGain(); + const releaseLength = 0.1; env.gain.value = 1; - const fadeLength = 0.1; - env.gain.value = 0.5; - env.gain.setValueAtTime(0.5, t + duration - fadeLength); - env.gain.linearRampToValueAtTime(0, t + duration); + env.gain.setValueAtTime(env.gain.value, t + duration); + env.gain.linearRampToValueAtTime(0, t + duration + releaseLength); + // env.gain.linearRampToValueAtTime(0, t + duration + releaseLength); chain.push(env); + bufferSource.stop(t + duration + releaseLength); + } else { + bufferSource.stop(t + duration); } } + // master out + const master = ac.createGain(); + master.gain.value = gain; + chain.push(master); + // filters cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance)); hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance)); @@ -204,9 +250,9 @@ Pattern.prototype.out = function () { chain.push(panner); } // master out - const master = ac.createGain(); + /* const master = ac.createGain(); master.gain.value = 0.8 * gain; - chain.push(master); + chain.push(master); */ chain.push(ac.destination); // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);