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:
parent
eca76dd7c4
commit
c1ce72469c
4 changed files with 91 additions and 25 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)];
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue