sounds onset and offset can be triggered independently

+ sounds tab now supports mousedown / mouseup to listen
This commit is contained in:
Felix Roos 2023-03-09 08:35:20 +01:00
parent cee08ea67d
commit 65e48c05f0
6 changed files with 103 additions and 48 deletions

View File

@ -6,14 +6,33 @@ export function gainNode(value) {
return node;
}
export const getOscillator = ({ s, freq, t, duration, release }) => {
export const getOscillator = ({ s, freq, t }) => {
// make oscillator
const o = getAudioContext().createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.start(t);
o.stop(t + duration + release);
return o;
//o.stop(t + duration + release);
const stop = (time) => o.stop(time);
return { node: o, stop };
};
// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future
export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
// sustain end
return {
node: gainNode,
stop: (t) => {
gainNode.gain.setValueAtTime(sustain * velocity, t);
gainNode.gain.linearRampToValueAtTime(0, t + release);
},
};
// gainNode.gain.linearRampToValueAtTime(0, end + release); // release
// return gainNode;
};
export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {

View File

@ -1,6 +1,6 @@
import { logger, toMidi, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext, setSound } from './index.mjs';
import { getADSR } from './helpers.mjs';
import { getEnvelope } from './helpers.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer>
@ -150,7 +150,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
}),
);
}
setSound(key, (options) => onTriggerSample(options, value), {
setSound(key, (t, hapValue) => onTriggerSample(t, hapValue, value), {
type: 'sample',
samples: value,
baseUrl,
@ -161,8 +161,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
const cutGroups = [];
export async function onTriggerSample(options, bank) {
const { hap, duration: hapDuration, t, cps } = options;
export async function onTriggerSample(t, value, bank) {
const {
s,
freq,
@ -176,10 +175,10 @@ export async function onTriggerSample(options, bank) {
speed = 1, // sample playback speed
begin = 0,
end = 1,
} = hap.value;
} = value;
const ac = getAudioContext();
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = hap.value;
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
// load sample
if (speed === 0) {
// no playback
@ -208,34 +207,41 @@ export async function onTriggerSample(options, bank) {
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
if (unit === 'c') {
// are there other units?
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * cps;
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * 1; //cps;
}
const shouldClip = /* soundfont || */ clip;
let duration = shouldClip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
// "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 * duration * bufferSource.playbackRate.value;
duration = (end - begin) * duration;
if (loop) {
const time = t + nudge;
const offset = begin * bufferSource.buffer.duration;
bufferSource.start(time, offset);
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
/*if (loop) {
// TODO: idea for loopBegin / loopEnd
// if one of [loopBegin,loopEnd] is <= 1, interpret it as normlized
// if [loopBegin,loopEnd] is bigger >= 1, interpret it as sample number
// this will simplify perfectly looping things, while still keeping the normalized option
// the only drawback is that looping between samples 0 and 1 is not possible (which is not real use case)
bufferSource.loop = true;
bufferSource.loopStart = offset;
bufferSource.loopEnd = offset + duration;
duration = loop * duration;
}
const time = t + nudge;
bufferSource.start(time, offset);
}*/
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
bufferSource.connect(envelope);
if (cut !== undefined) {
cutGroups[cut]?.stop(time); // fade out?
cutGroups[cut] = bufferSource;
}
//chain.push(bufferSource);
bufferSource.stop(t + duration + release);
const adsr = getADSR(attack, decay, sustain, release, 1, time, time + duration);
bufferSource.connect(adsr);
//chain.push(adsr);
return adsr;
const stop = (endTime) => {
let releaseTime = endTime;
if (!clip) {
releaseTime = t + (end - begin) * bufferDuration;
}
bufferSource.stop(releaseTime + release);
releaseEnvelope(releaseTime);
};
return { node: envelope, stop };
}
/*const getSoundfontKey = (s) => {

View File

@ -1,15 +1,15 @@
import { fromMidi, toMidi } from '@strudel.cycles/core';
import { setSound } from './webaudio.mjs';
import { getOscillator, gainNode, getADSR } from './helpers.mjs';
import { getOscillator, gainNode, getEnvelope } from './helpers.mjs';
export function registerSynthSounds() {
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
setSound(
wave,
({ hap, duration, t }) => {
(t, value) => {
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = hap.value;
let { n, note, freq } = hap.value;
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
let { n, note, freq } = value;
// with synths, n and note are the same thing
n = note || n || 36;
if (typeof n === 'string') {
@ -21,16 +21,17 @@ export function registerSynthSounds() {
}
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
// make oscillator
const o = getOscillator({ t, s: wave, freq, duration, release });
// chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
//chain.push(gainNode(0.3));
const { node: o, stop } = getOscillator({ t, s: wave, freq });
const g = gainNode(0.3);
// TODO: make adsr work with samples without pops
// envelope
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration);
//chain.push(adsr);
return o.connect(g).connect(adsr);
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
return {
node: o.connect(g).connect(envelope),
stop: (t) => {
releaseEnvelope(t);
stop(t + release);
},
};
},
{ type: 'synth', prebake: true },
);

View File

@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import * as strudel from '@strudel.cycles/core';
import './feedbackdelay.mjs';
import './reverb.mjs';
const { Pattern } = strudel;
const { Pattern, logger } = strudel;
import './vowel.mjs';
import workletsUrl from './worklets.mjs?url';
import { getFilter, gainNode } from './helpers.mjs';
@ -125,7 +125,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
// calculate absolute time
let t = ac.currentTime + deadline;
// destructure value
// destructure
let {
s = 'triangle',
bank,
@ -161,17 +161,29 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
s = `${bank}_${s}`;
}
// get source AudioNode
let node;
const options = { hap, t, deadline, duration: hapDuration, cps };
let sourceNode;
if (source) {
node = source(options);
sourceNode = source(t, hap.value);
} else if (soundMap.get()[s]) {
const { onTrigger } = soundMap.get()[s];
node = await onTrigger(options);
const soundHandle = await onTrigger(t, hap.value);
if (soundHandle) {
sourceNode = soundHandle.node;
soundHandle.stop(t + hapDuration);
}
} else {
throw new Error(`sound ${s} not found! Is it loaded?`);
}
chain.push(node);
if (!sourceNode) {
// if onTrigger does not return anything, we will just silently skip
// this can be used for things like speed(0) in the sampler
return;
}
if (ac.currentTime > t) {
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
return;
}
chain.push(sourceNode);
// gain stage
chain.push(gainNode(gain));

View File

@ -62,6 +62,7 @@ export function loadWebDirt(config) {
* @noAutocomplete
*/
Pattern.prototype.webdirt = function () {
throw new Error('webdirt support has been dropped..');
// create a WebDirt object and initialize Web Audio context
/* return this.onTrigger(async (time, e, currentTime) => {
if (!webDirt) {

View File

@ -7,7 +7,7 @@ import React, { useMemo, useCallback, useLayoutEffect, useRef, useState } from '
import { Reference } from './Reference';
import { themes } from './themes.mjs';
import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../settings.mjs';
import { soundMap } from '@strudel.cycles/webaudio';
import { getAudioContext, soundMap } from '@strudel.cycles/webaudio';
import { useStore } from '@nanostores/react';
export function Footer({ context }) {
@ -238,6 +238,12 @@ function SoundsTab() {
}
return Object.entries(sounds);
}, [sounds, soundsFilter]);
// holds mutable ref to current triggered sound
const trigRef = useRef();
// stop current sound on mouseup
useEvent('mouseup', () => {
trigRef.current?.then((ref) => ref?.stop(getAudioContext().currentTime + 0.01));
});
return (
<div id="sounds-tab" className="break-normal w-full px-4 dark:text-white text-stone-900">
<ButtonGroup
@ -245,9 +251,19 @@ function SoundsTab() {
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
items={{ all: 'All', hideDefaults: 'Hide Defaults' }}
></ButtonGroup>
<div className="pt-4">
{soundEntries.map(([name, { data }]) => (
<span key={name} className="cursor-pointer hover:opacity-50" onClick={() => {}}>
<div className="pt-4 select-none">
{soundEntries.map(([name, { data, onTrigger }]) => (
<span
key={name}
className="cursor-pointer hover:opacity-50"
onMouseDown={async () => {
const ctx = getAudioContext();
trigRef.current = Promise.resolve(onTrigger(ctx.currentTime + 0.05, { freq: 220, s: name, clip: 1 }));
trigRef.current.then((ref) => {
ref?.node.connect(ctx.destination);
});
}}
>
{' '}
{name}
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}