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
This commit is contained in:
Felix Roos 2022-06-27 00:01:28 +02:00
parent eca76dd7c4
commit c1ce72469c
4 changed files with 91 additions and 25 deletions

View file

@ -744,6 +744,7 @@ const generic_params = [
['f', 'uid', ''], ['f', 'uid', ''],
['f', 'val', ''], ['f', 'val', ''],
['f', 'cps', ''], ['f', 'cps', ''],
['f', 'choke', ''],
]; ];
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13 // TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13

View file

@ -16,6 +16,9 @@ async function loadFont(name) {
} }
export async function getFontBufferSource(name, pitch, ac) { export async function getFontBufferSource(name, pitch, ac) {
if (typeof pitch === 'string') {
pitch = toMidi(pitch);
}
const { buffer, zone } = await getFontPitch(name, pitch, ac); const { buffer, zone } = await getFontPitch(name, pitch, ac);
const src = ac.createBufferSource(); const src = ac.createBufferSource();
src.buffer = buffer; src.buffer = buffer;

View file

@ -97,12 +97,28 @@ export const samples = (sampleMap, baseUrl = sampleMap._base) => {
sampleCache.current = { sampleCache.current = {
...sampleCache.current, ...sampleCache.current,
...Object.fromEntries( ...Object.fromEntries(
Object.entries(sampleMap).map(([key, value]) => [ Object.entries(sampleMap).map(([key, value]) => {
key, if (typeof value === 'string') {
(typeof value === 'string' ? [value] : value).map((v) => return [key, [value]];
(baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/'), }
), 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)];
}),
),
];
}),
), ),
}; };
}; };

View file

@ -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 { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
import * as strudel 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'; import { loadBuffer } from './sampler.mjs';
const { Pattern } = strudel; const { Pattern } = strudel;
@ -68,7 +68,11 @@ const getSoundfontKey = (s) => {
return; 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(); const ac = getAudioContext();
// is sample from loaded samples(..) // is sample from loaded samples(..)
const samples = getLoadedSamples(); const samples = getLoadedSamples();
@ -79,10 +83,33 @@ const getSampleBufferSource = async (s, n) => {
if (!bank) { if (!bank) {
throw new Error('sample not found:', s, 'try one of ' + Object.keys(samples)); 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 buffer = await loadBuffer(sampleUrl, ac);
const bufferSource = ac.createBufferSource(); const bufferSource = ac.createBufferSource();
bufferSource.buffer = buffer; 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; return bufferSource;
}; };
@ -96,7 +123,9 @@ Pattern.prototype.out = function () {
freq, freq,
s, s,
sf, sf,
choke = 0, // if 1, samples will be cut off when the hap ends
n = 0, n = 0,
note,
gain = 1, gain = 1,
cutoff, cutoff,
resonance = 1, resonance = 1,
@ -106,19 +135,29 @@ Pattern.prototype.out = function () {
bandq = 1, bandq = 1,
pan, pan,
attack = 0.001, attack = 0.001,
decay = 0, decay = 0.05,
sustain = 1, sustain = 0.5,
release = 0.001, release = 0.001,
speed = 1, // sample playback speed speed = 1, // sample playback speed
begin = 0, begin = 0,
end = 1, end = 1,
} = hap.value; } = 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 // the chain will hold all audio nodes that connect to each other
const chain = []; const chain = [];
if (typeof n === 'string') { if (typeof s === 'string' && s.includes(':')) {
n = toMidi(n); // e.g. c3 => 48 [s, n] = s.split(':');
}
if (typeof note === 'string' && note.includes(':')) {
[note, n] = note.split(':');
} }
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) { 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 // get frequency
if (!freq && typeof n === 'number') { if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48); freq = fromMidi(n); // + 48);
@ -150,10 +189,10 @@ Pattern.prototype.out = function () {
try { try {
if (soundfont) { if (soundfont) {
// is soundfont // is soundfont
bufferSource = await globalThis.getFontBufferSource(soundfont, n, ac); bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
} else { } else {
// is sample from loaded samples(..) // is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n); bufferSource = await getSampleBufferSource(s, n, note);
} }
} catch (err) { } catch (err) {
console.warn(err); console.warn(err);
@ -170,27 +209,34 @@ Pattern.prototype.out = function () {
} }
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value; bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
// TODO: nudge, unit, cut, loop // 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; // let duration = bufferSource.buffer.duration;
const offset = begin * duration; const offset = begin * duration;
duration = ((end - begin) * duration) / Math.abs(speed); duration = ((end - begin) * duration) / Math.abs(speed);
if (soundfont) { if (soundfont || choke) {
bufferSource.start(t, offset); // duration does not work here for some reason bufferSource.start(t, offset); // duration does not work here for some reason
} else { } else {
bufferSource.start(t, offset, duration); bufferSource.start(t, offset, duration);
} }
bufferSource.stop(t + duration);
chain.push(bufferSource); chain.push(bufferSource);
if (soundfont) { if (soundfont || choke) {
const env = ac.createGain(); const env = ac.createGain();
const releaseLength = 0.1;
env.gain.value = 1; env.gain.value = 1;
const fadeLength = 0.1; env.gain.setValueAtTime(env.gain.value, t + duration);
env.gain.value = 0.5; env.gain.linearRampToValueAtTime(0, t + duration + releaseLength);
env.gain.setValueAtTime(0.5, t + duration - fadeLength); // env.gain.linearRampToValueAtTime(0, t + duration + releaseLength);
env.gain.linearRampToValueAtTime(0, t + duration);
chain.push(env); 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 // filters
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance)); cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance)); hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
@ -204,9 +250,9 @@ Pattern.prototype.out = function () {
chain.push(panner); chain.push(panner);
} }
// master out // master out
const master = ac.createGain(); /* const master = ac.createGain();
master.gain.value = 0.8 * gain; master.gain.value = 0.8 * gain;
chain.push(master); chain.push(master); */
chain.push(ac.destination); chain.push(ac.destination);
// connect chain elements together // connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);