basic scope feature
This commit is contained in:
parent
f20c5a1bcb
commit
7370f41fa0
5 changed files with 91 additions and 2 deletions
|
|
@ -146,6 +146,9 @@ const generic_params = [
|
||||||
*/
|
*/
|
||||||
['bank'],
|
['bank'],
|
||||||
|
|
||||||
|
['analyze'], // sends
|
||||||
|
['fft'],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level.
|
* Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level.
|
||||||
* Note that the decay is only audible if the sustain value is lower than 1.
|
* Note that the decay is only audible if the sustain value is lower than 1.
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const getDrawContext = (id = 'test-canvas') => {
|
||||||
return canvas.getContext('2d');
|
return canvas.getContext('2d');
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
|
||||||
if (window.strudelAnimation) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export * from './cyclist.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './time.mjs';
|
export * from './time.mjs';
|
||||||
export * from './draw.mjs';
|
export * from './draw.mjs';
|
||||||
|
export * from './scope.mjs';
|
||||||
export * from './animate.mjs';
|
export * from './animate.mjs';
|
||||||
export * from './pianoroll.mjs';
|
export * from './pianoroll.mjs';
|
||||||
export * from './spiral.mjs';
|
export * from './spiral.mjs';
|
||||||
|
|
|
||||||
48
packages/core/scope.mjs
Normal file
48
packages/core/scope.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Pattern } from './pattern.mjs';
|
||||||
|
import { getDrawContext } from './draw.mjs';
|
||||||
|
import { analyser } from '@strudel.cycles/webaudio';
|
||||||
|
|
||||||
|
export function drawTimeScope(analyser, dataArray, { align = true, color = 'white', thickness = 2 } = {}) {
|
||||||
|
const canvasCtx = getDrawContext();
|
||||||
|
|
||||||
|
canvasCtx.lineWidth = thickness;
|
||||||
|
canvasCtx.strokeStyle = color;
|
||||||
|
|
||||||
|
canvasCtx.beginPath();
|
||||||
|
let canvas = canvasCtx.canvas;
|
||||||
|
|
||||||
|
const bufferSize = analyser.frequencyBinCount;
|
||||||
|
const triggerValue = 256 / 2;
|
||||||
|
const triggerIndex = align
|
||||||
|
? Array.from(dataArray).findIndex((v, i, arr) => i && arr[i - 1] < triggerValue && v >= triggerValue)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const sliceWidth = (canvas.width * 1.0) / bufferSize;
|
||||||
|
let x = 0;
|
||||||
|
|
||||||
|
for (let i = triggerIndex; i < bufferSize; i++) {
|
||||||
|
const v = dataArray[i] / 128.0;
|
||||||
|
const y = (v * (canvas.height / 2)) / 2 + canvas.height / 2;
|
||||||
|
if (i === 0) {
|
||||||
|
canvasCtx.moveTo(x, y);
|
||||||
|
} else {
|
||||||
|
canvasCtx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
x += sliceWidth;
|
||||||
|
}
|
||||||
|
canvasCtx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern.prototype.scope = function (config = {}) {
|
||||||
|
return this.analyze(1).draw((ctx) => {
|
||||||
|
let data = getAnalyzerData('time');
|
||||||
|
const { smear = 0 } = config;
|
||||||
|
if (!smear) {
|
||||||
|
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = `rgba(0,0,0,${1 - smear})`;
|
||||||
|
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
|
||||||
|
}
|
||||||
|
data && drawTimeScope(analyser, data, config);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -121,6 +121,34 @@ function getReverb(orbit, duration = 2) {
|
||||||
return reverbs[orbit];
|
return reverbs[orbit];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let analyser, analyserData /* s = {} */;
|
||||||
|
export function getAnalyser(/* orbit, */ fftSize = 2048) {
|
||||||
|
if (!analyser /*s [orbit] */) {
|
||||||
|
const analyserNode = getAudioContext().createAnalyser();
|
||||||
|
analyserNode.fftSize = fftSize;
|
||||||
|
// getDestination().connect(analyserNode);
|
||||||
|
analyser /* s[orbit] */ = analyserNode;
|
||||||
|
analyserData = new Uint8Array(analyser.frequencyBinCount);
|
||||||
|
}
|
||||||
|
if (analyser /* s[orbit] */.fftSize !== fftSize) {
|
||||||
|
analyser /* s[orbit] */.fftSize = fftSize;
|
||||||
|
analyserData = new Uint8Array(analyser.frequencyBinCount);
|
||||||
|
}
|
||||||
|
return analyser /* s[orbit] */;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAnalyzerData(type = 'time') {
|
||||||
|
const getter = {
|
||||||
|
time: () => analyser?.getByteTimeDomainData(analyserData),
|
||||||
|
frequency: () => analyser?.getByteFrequencyData(analyserData),
|
||||||
|
}[type];
|
||||||
|
if (!getter) {
|
||||||
|
throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`);
|
||||||
|
}
|
||||||
|
getter();
|
||||||
|
return analyserData;
|
||||||
|
}
|
||||||
|
|
||||||
function effectSend(input, effect, wet) {
|
function effectSend(input, effect, wet) {
|
||||||
const send = gainNode(wet);
|
const send = gainNode(wet);
|
||||||
input.connect(send);
|
input.connect(send);
|
||||||
|
|
@ -167,6 +195,8 @@ export const superdough = async (value, deadline, hapDuration) => {
|
||||||
room,
|
room,
|
||||||
size = 2,
|
size = 2,
|
||||||
velocity = 1,
|
velocity = 1,
|
||||||
|
analyze, // analyser wet
|
||||||
|
fft = 8, // fftSize 0 - 10
|
||||||
} = value;
|
} = value;
|
||||||
gain *= velocity; // legacy fix for velocity
|
gain *= velocity; // legacy fix for velocity
|
||||||
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
|
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
|
||||||
|
|
@ -241,12 +271,19 @@ export const superdough = async (value, deadline, hapDuration) => {
|
||||||
reverbSend = effectSend(post, reverbNode, room);
|
reverbSend = effectSend(post, reverbNode, room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// analyser
|
||||||
|
let analyserSend;
|
||||||
|
if (analyze) {
|
||||||
|
const analyserNode = getAnalyser(/* orbit, */ 2 ** (fft + 5));
|
||||||
|
analyserSend = effectSend(post, analyserNode, analyze);
|
||||||
|
}
|
||||||
|
|
||||||
// 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]);
|
||||||
|
|
||||||
// toDisconnect = all the node that should be disconnected in onended callback
|
// toDisconnect = all the node that should be disconnected in onended callback
|
||||||
// this is crucial for performance
|
// this is crucial for performance
|
||||||
toDisconnect = chain.concat([delaySend, reverbSend]);
|
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps);
|
export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue