mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-25 04:28:30 +00:00
sounds onset and offset can be triggered independently
+ sounds tab now supports mousedown / mouseup to listen
This commit is contained in:
parent
cee08ea67d
commit
65e48c05f0
@ -6,14 +6,33 @@ export function gainNode(value) {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOscillator = ({ s, freq, t, duration, release }) => {
|
export const getOscillator = ({ s, freq, t }) => {
|
||||||
// make oscillator
|
// make oscillator
|
||||||
const o = getAudioContext().createOscillator();
|
const o = getAudioContext().createOscillator();
|
||||||
o.type = s || 'triangle';
|
o.type = s || 'triangle';
|
||||||
o.frequency.value = Number(freq);
|
o.frequency.value = Number(freq);
|
||||||
o.start(t);
|
o.start(t);
|
||||||
o.stop(t + duration + release);
|
//o.stop(t + duration + release);
|
||||||
return o;
|
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) => {
|
export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { logger, toMidi, valueToMidi } from '@strudel.cycles/core';
|
import { logger, toMidi, valueToMidi } from '@strudel.cycles/core';
|
||||||
import { getAudioContext, setSound } from './index.mjs';
|
import { getAudioContext, setSound } from './index.mjs';
|
||||||
import { getADSR } from './helpers.mjs';
|
import { getEnvelope } from './helpers.mjs';
|
||||||
|
|
||||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||||
const loadCache = {}; // 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',
|
type: 'sample',
|
||||||
samples: value,
|
samples: value,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
@ -161,8 +161,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
|
|||||||
|
|
||||||
const cutGroups = [];
|
const cutGroups = [];
|
||||||
|
|
||||||
export async function onTriggerSample(options, bank) {
|
export async function onTriggerSample(t, value, bank) {
|
||||||
const { hap, duration: hapDuration, t, cps } = options;
|
|
||||||
const {
|
const {
|
||||||
s,
|
s,
|
||||||
freq,
|
freq,
|
||||||
@ -176,10 +175,10 @@ export async function onTriggerSample(options, bank) {
|
|||||||
speed = 1, // sample playback speed
|
speed = 1, // sample playback speed
|
||||||
begin = 0,
|
begin = 0,
|
||||||
end = 1,
|
end = 1,
|
||||||
} = hap.value;
|
} = value;
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
// destructure adsr here, because the default should be different for synths and samples
|
// 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
|
// load sample
|
||||||
if (speed === 0) {
|
if (speed === 0) {
|
||||||
// no playback
|
// no playback
|
||||||
@ -208,34 +207,41 @@ export async function onTriggerSample(options, bank) {
|
|||||||
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
|
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
|
||||||
if (unit === 'c') {
|
if (unit === 'c') {
|
||||||
// are there other units?
|
// 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,
|
// "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,
|
// 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."
|
// the midway point through a 10-second audio buffer is still 5."
|
||||||
const offset = begin * duration * bufferSource.playbackRate.value;
|
const time = t + nudge;
|
||||||
duration = (end - begin) * duration;
|
const offset = begin * bufferSource.buffer.duration;
|
||||||
if (loop) {
|
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.loop = true;
|
||||||
bufferSource.loopStart = offset;
|
bufferSource.loopStart = offset;
|
||||||
bufferSource.loopEnd = offset + duration;
|
bufferSource.loopEnd = offset + duration;
|
||||||
duration = loop * duration;
|
duration = loop * duration;
|
||||||
}
|
}*/
|
||||||
const time = t + nudge;
|
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
|
||||||
|
bufferSource.connect(envelope);
|
||||||
bufferSource.start(time, offset);
|
|
||||||
if (cut !== undefined) {
|
if (cut !== undefined) {
|
||||||
cutGroups[cut]?.stop(time); // fade out?
|
cutGroups[cut]?.stop(time); // fade out?
|
||||||
cutGroups[cut] = bufferSource;
|
cutGroups[cut] = bufferSource;
|
||||||
}
|
}
|
||||||
//chain.push(bufferSource);
|
const stop = (endTime) => {
|
||||||
bufferSource.stop(t + duration + release);
|
let releaseTime = endTime;
|
||||||
const adsr = getADSR(attack, decay, sustain, release, 1, time, time + duration);
|
if (!clip) {
|
||||||
bufferSource.connect(adsr);
|
releaseTime = t + (end - begin) * bufferDuration;
|
||||||
//chain.push(adsr);
|
}
|
||||||
return adsr;
|
bufferSource.stop(releaseTime + release);
|
||||||
|
releaseEnvelope(releaseTime);
|
||||||
|
};
|
||||||
|
return { node: envelope, stop };
|
||||||
}
|
}
|
||||||
|
|
||||||
/*const getSoundfontKey = (s) => {
|
/*const getSoundfontKey = (s) => {
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { fromMidi, toMidi } from '@strudel.cycles/core';
|
import { fromMidi, toMidi } from '@strudel.cycles/core';
|
||||||
import { setSound } from './webaudio.mjs';
|
import { setSound } from './webaudio.mjs';
|
||||||
import { getOscillator, gainNode, getADSR } from './helpers.mjs';
|
import { getOscillator, gainNode, getEnvelope } from './helpers.mjs';
|
||||||
|
|
||||||
export function registerSynthSounds() {
|
export function registerSynthSounds() {
|
||||||
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
|
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
|
||||||
setSound(
|
setSound(
|
||||||
wave,
|
wave,
|
||||||
({ hap, duration, t }) => {
|
(t, value) => {
|
||||||
// destructure adsr here, because the default should be different for synths and samples
|
// 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;
|
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
|
||||||
let { n, note, freq } = hap.value;
|
let { n, note, freq } = value;
|
||||||
// with synths, n and note are the same thing
|
// with synths, n and note are the same thing
|
||||||
n = note || n || 36;
|
n = note || n || 36;
|
||||||
if (typeof n === 'string') {
|
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)
|
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
|
||||||
// make oscillator
|
// make oscillator
|
||||||
const o = getOscillator({ t, s: wave, freq, duration, release });
|
const { node: o, stop } = getOscillator({ t, s: wave, freq });
|
||||||
// chain.push(o);
|
|
||||||
// level down oscillators as they are really loud compared to samples i've tested
|
|
||||||
//chain.push(gainNode(0.3));
|
|
||||||
const g = gainNode(0.3);
|
const g = gainNode(0.3);
|
||||||
// TODO: make adsr work with samples without pops
|
|
||||||
// envelope
|
// envelope
|
||||||
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + duration);
|
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
|
||||||
//chain.push(adsr);
|
return {
|
||||||
return o.connect(g).connect(adsr);
|
node: o.connect(g).connect(envelope),
|
||||||
|
stop: (t) => {
|
||||||
|
releaseEnvelope(t);
|
||||||
|
stop(t + release);
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
{ type: 'synth', prebake: true },
|
{ type: 'synth', prebake: true },
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 * as strudel from '@strudel.cycles/core';
|
||||||
import './feedbackdelay.mjs';
|
import './feedbackdelay.mjs';
|
||||||
import './reverb.mjs';
|
import './reverb.mjs';
|
||||||
const { Pattern } = strudel;
|
const { Pattern, logger } = strudel;
|
||||||
import './vowel.mjs';
|
import './vowel.mjs';
|
||||||
import workletsUrl from './worklets.mjs?url';
|
import workletsUrl from './worklets.mjs?url';
|
||||||
import { getFilter, gainNode } from './helpers.mjs';
|
import { getFilter, gainNode } from './helpers.mjs';
|
||||||
@ -125,7 +125,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
|
|||||||
|
|
||||||
// calculate absolute time
|
// calculate absolute time
|
||||||
let t = ac.currentTime + deadline;
|
let t = ac.currentTime + deadline;
|
||||||
// destructure value
|
// destructure
|
||||||
let {
|
let {
|
||||||
s = 'triangle',
|
s = 'triangle',
|
||||||
bank,
|
bank,
|
||||||
@ -161,17 +161,29 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
|
|||||||
s = `${bank}_${s}`;
|
s = `${bank}_${s}`;
|
||||||
}
|
}
|
||||||
// get source AudioNode
|
// get source AudioNode
|
||||||
let node;
|
let sourceNode;
|
||||||
const options = { hap, t, deadline, duration: hapDuration, cps };
|
|
||||||
if (source) {
|
if (source) {
|
||||||
node = source(options);
|
sourceNode = source(t, hap.value);
|
||||||
} else if (soundMap.get()[s]) {
|
} else if (soundMap.get()[s]) {
|
||||||
const { onTrigger } = 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 {
|
} else {
|
||||||
throw new Error(`sound ${s} not found! Is it loaded?`);
|
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
|
// gain stage
|
||||||
chain.push(gainNode(gain));
|
chain.push(gainNode(gain));
|
||||||
|
|||||||
@ -62,6 +62,7 @@ export function loadWebDirt(config) {
|
|||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.webdirt = function () {
|
Pattern.prototype.webdirt = function () {
|
||||||
|
throw new Error('webdirt support has been dropped..');
|
||||||
// create a WebDirt object and initialize Web Audio context
|
// create a WebDirt object and initialize Web Audio context
|
||||||
/* return this.onTrigger(async (time, e, currentTime) => {
|
/* return this.onTrigger(async (time, e, currentTime) => {
|
||||||
if (!webDirt) {
|
if (!webDirt) {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import React, { useMemo, useCallback, useLayoutEffect, useRef, useState } from '
|
|||||||
import { Reference } from './Reference';
|
import { Reference } from './Reference';
|
||||||
import { themes } from './themes.mjs';
|
import { themes } from './themes.mjs';
|
||||||
import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../settings.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';
|
import { useStore } from '@nanostores/react';
|
||||||
|
|
||||||
export function Footer({ context }) {
|
export function Footer({ context }) {
|
||||||
@ -238,6 +238,12 @@ function SoundsTab() {
|
|||||||
}
|
}
|
||||||
return Object.entries(sounds);
|
return Object.entries(sounds);
|
||||||
}, [sounds, soundsFilter]);
|
}, [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 (
|
return (
|
||||||
<div id="sounds-tab" className="break-normal w-full px-4 dark:text-white text-stone-900">
|
<div id="sounds-tab" className="break-normal w-full px-4 dark:text-white text-stone-900">
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
@ -245,9 +251,19 @@ function SoundsTab() {
|
|||||||
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
|
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
|
||||||
items={{ all: 'All', hideDefaults: 'Hide Defaults' }}
|
items={{ all: 'All', hideDefaults: 'Hide Defaults' }}
|
||||||
></ButtonGroup>
|
></ButtonGroup>
|
||||||
<div className="pt-4">
|
<div className="pt-4 select-none">
|
||||||
{soundEntries.map(([name, { data }]) => (
|
{soundEntries.map(([name, { data, onTrigger }]) => (
|
||||||
<span key={name} className="cursor-pointer hover:opacity-50" onClick={() => {}}>
|
<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}
|
{name}
|
||||||
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
|
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user