Merge branch 'main' into glossing/wavetable-synth
This commit is contained in:
commit
5838ade740
11 changed files with 359 additions and 78 deletions
|
|
@ -107,7 +107,6 @@ function getModulationShapeInput(val) {
|
|||
export function getLfo(audioContext, begin, end, properties = {}) {
|
||||
const { shape = 0, ...props } = properties;
|
||||
const { dcoffset = -0.5, depth = 1 } = properties;
|
||||
debugger;
|
||||
const lfoprops = {
|
||||
frequency: 1,
|
||||
depth,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
|
||||
import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs';
|
||||
import { getAudioContext, registerSound, registerWaveTable } from './index.mjs';
|
||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
|
|
@ -22,39 +22,16 @@ function humanFileSize(bytes, si) {
|
|||
return bytes.toFixed(1) + ' ' + units[u];
|
||||
}
|
||||
|
||||
// deduces relevant info for sample loading from hap.value and sample definition
|
||||
// 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)
|
||||
export function getSampleInfo(hapValue, bank) {
|
||||
const { s, n = 0, speed = 1.0 } = hapValue;
|
||||
let midi = valueToMidi(hapValue, 36);
|
||||
let transpose = midi - 36; // C3 is middle C;
|
||||
let sampleUrl;
|
||||
let index = 0;
|
||||
if (Array.isArray(bank)) {
|
||||
index = getSoundIndex(n, bank.length);
|
||||
sampleUrl = bank[index];
|
||||
} else {
|
||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||
// 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
|
||||
index = getSoundIndex(n, bank[closest].length);
|
||||
sampleUrl = bank[closest][index];
|
||||
}
|
||||
const label = `${s}:${index}`;
|
||||
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, sampleUrl, index, midi, label, playbackRate };
|
||||
return { transpose, url, index, midi, label, playbackRate };
|
||||
}
|
||||
|
||||
// takes hapValue and returns buffer + playbackRate.
|
||||
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
|
||||
let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
|
||||
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
|
||||
if (resolveUrl) {
|
||||
sampleUrl = await resolveUrl(sampleUrl);
|
||||
}
|
||||
|
|
@ -79,14 +56,14 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
|||
bufferSource.buffer = buffer;
|
||||
bufferSource.playbackRate.value = playbackRate;
|
||||
|
||||
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
||||
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
||||
|
||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
||||
// the midway point through a 10-second audio buffer is still 5."
|
||||
const offset = begin * bufferSource.buffer.duration;
|
||||
|
||||
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
|
||||
const loop = hapValue.loop;
|
||||
if (loop) {
|
||||
bufferSource.loop = true;
|
||||
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
||||
|
|
@ -267,16 +244,12 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
|
|||
return samples(json, baseUrl || base, options);
|
||||
}
|
||||
const { prebake, tag } = options;
|
||||
|
||||
processSampleMap(
|
||||
sampleMap,
|
||||
(key, bank) =>
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
|
||||
type: 'sample',
|
||||
samples: bank,
|
||||
baseUrl,
|
||||
prebake,
|
||||
tag,
|
||||
}),
|
||||
(key, bank) => {
|
||||
registerSampleSource(key, bank, { baseUrl, prebake, tag });
|
||||
},
|
||||
baseUrl,
|
||||
);
|
||||
};
|
||||
|
|
@ -366,3 +339,20 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||
|
||||
return handle;
|
||||
}
|
||||
|
||||
function registerSample(key, bank, params) {
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
|
||||
type: 'sample',
|
||||
samples: bank,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
export function registerSampleSource(key, bank, params) {
|
||||
const isWavetable = key.startsWith('wt_');
|
||||
if (isWavetable) {
|
||||
registerWaveTable(key, bank, params);
|
||||
} else {
|
||||
registerSample(key, bank, params);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,3 +76,32 @@ export function cycleToSeconds(cycle, cps) {
|
|||
export function secondsToCycle(t, cps) {
|
||||
return t * cps;
|
||||
}
|
||||
|
||||
// deduces relevant info for sample loading from hap.value and sample definition
|
||||
// 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)
|
||||
export function getCommonSampleInfo(hapValue, bank) {
|
||||
const { s, n = 0 } = hapValue;
|
||||
let midi = valueToMidi(hapValue, 36);
|
||||
let transpose = midi - 36; // C3 is middle C;
|
||||
let url;
|
||||
let index = 0;
|
||||
if (Array.isArray(bank)) {
|
||||
index = getSoundIndex(n, bank.length);
|
||||
url = bank[index];
|
||||
} else {
|
||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||
// 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
|
||||
index = getSoundIndex(n, bank[closest].length);
|
||||
url = bank[closest][index];
|
||||
}
|
||||
const label = `${s}:${index}`;
|
||||
return { transpose, url, index, midi, label };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { getSoundIndex, valueToMidi } from './util.mjs';
|
||||
import { getCommonSampleInfo } from './util.mjs';
|
||||
import {
|
||||
destroyAudioWorkletNode,
|
||||
getADSRValues,
|
||||
|
|
@ -39,11 +39,11 @@ export const WarpMode = Object.freeze({
|
|||
FLIP: 21,
|
||||
});
|
||||
|
||||
async function loadWavetableFrames(url, label, frameLen = 256) {
|
||||
async function loadWavetableFrames(url, label, frameLen = 2048) {
|
||||
const buf = await loadBuffer(url, label);
|
||||
const ch0 = buf.getChannelData(0);
|
||||
const total = ch0.length;
|
||||
const numFrames = Math.floor(total / frameLen);
|
||||
const numFrames = Math.max(1, Math.floor(total / frameLen));
|
||||
const frames = new Array(numFrames);
|
||||
for (let i = 0; i < numFrames; i++) {
|
||||
const start = i * frameLen;
|
||||
|
|
@ -86,16 +86,6 @@ function humanFileSize(bytes, si) {
|
|||
return bytes.toFixed(1) + ' ' + units[u];
|
||||
}
|
||||
|
||||
export function getTableInfo(hapValue, tableUrls) {
|
||||
const { s, n = 0 } = hapValue;
|
||||
let midi = valueToMidi(hapValue, 36);
|
||||
let transpose = midi - 36; // C3 is middle C;
|
||||
const index = getSoundIndex(n, tableUrls.length);
|
||||
const tableUrl = tableUrls[index];
|
||||
const label = `${s}:${index}`;
|
||||
return { transpose, tableUrl, index, midi, label };
|
||||
}
|
||||
|
||||
// Extract the sample rate of a .wav file
|
||||
function parseWavSampleRate(arrBuf) {
|
||||
const dv = new DataView(arrBuf);
|
||||
|
|
@ -181,19 +171,19 @@ const _processTables = (json, baseUrl, frameLen, options = {}) => {
|
|||
return true;
|
||||
});
|
||||
if (tables.length) {
|
||||
const { prebake, tag } = options;
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, tables, frameLen), {
|
||||
type: 'wavetable',
|
||||
tables,
|
||||
baseUrl,
|
||||
frameLen,
|
||||
prebake,
|
||||
tag,
|
||||
});
|
||||
registerWaveTable(key, tables, { baseUrl, frameLen });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export function registerWaveTable(key, tables, params) {
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, tables, params?.frameLen ?? 2048), {
|
||||
type: 'wavetable',
|
||||
tables,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a collection of wavetables to use with `s`
|
||||
*
|
||||
|
|
@ -224,7 +214,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
|
|||
});
|
||||
};
|
||||
|
||||
async function onTriggerSynth(t, value, onended, tables, frameLen) {
|
||||
export async function onTriggerSynth(t, value, onended, tables, frameLen) {
|
||||
const { s, n = 0, duration } = value;
|
||||
const ac = getAudioContext();
|
||||
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||
|
|
@ -233,8 +223,8 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) {
|
|||
wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE;
|
||||
}
|
||||
const frequency = getFrequencyFromValue(value);
|
||||
const { tableUrl, label } = getTableInfo(value, tables);
|
||||
const payload = await loadWavetableFrames(tableUrl, label, frameLen);
|
||||
const { url, label } = getCommonSampleInfo(value, tables);
|
||||
const payload = await loadWavetableFrames(url, label, frameLen);
|
||||
const holdEnd = t + duration;
|
||||
const endWithRelease = holdEnd + release;
|
||||
const envEnd = endWithRelease + 0.01;
|
||||
|
|
@ -265,6 +255,7 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) {
|
|||
const wtParams = source.parameters;
|
||||
const positionParam = wtParams.get('position');
|
||||
const warpParam = wtParams.get('warp');
|
||||
let posLFO;
|
||||
if (posADSRParams.some((p) => p !== undefined)) {
|
||||
const [pAttack, pDecay, pSustain, pRelease] = getADSRValues(posADSRParams);
|
||||
getParamADSR(positionParam, pAttack, pDecay, pSustain, pRelease, 0, 1, t, holdEnd, 'linear');
|
||||
|
|
@ -278,6 +269,7 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) {
|
|||
});
|
||||
posLFO.connect(positionParam);
|
||||
}
|
||||
let warpLFO;
|
||||
if (posADSRParams.some((p) => p !== undefined)) {
|
||||
const [wAttack, wDecay, wSustain, wRelease] = getADSRValues(warpADSRParams);
|
||||
getParamADSR(warpParam, wAttack, wDecay, wSustain, wRelease, 0, 1, t, holdEnd, 'linear');
|
||||
|
|
@ -304,8 +296,8 @@ async function onTriggerSynth(t, value, onended, tables, frameLen) {
|
|||
destroyAudioWorkletNode(source);
|
||||
vibratoOscillator?.stop();
|
||||
node.disconnect();
|
||||
warpLFO.disconnect();
|
||||
posLFO.disconnect();
|
||||
posLFO?.disconnect();
|
||||
warpLFO?.disconnect();
|
||||
onended();
|
||||
},
|
||||
t,
|
||||
|
|
|
|||
|
|
@ -1049,7 +1049,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||
{ name: 'begin', 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: 'detune', defaultValue: 0 },
|
||||
{ name: 'detune', defaultValue: 0.18 },
|
||||
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||
{ name: 'warpMode', defaultValue: 0 },
|
||||
|
|
@ -1239,6 +1239,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||
}
|
||||
const outL = outputs[0][0];
|
||||
const outR = outputs[0][1] || outputs[0][0];
|
||||
const gainAdjustment = 0.3;
|
||||
|
||||
if (!this.tables) {
|
||||
outL.fill(0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue