From 648fbf99fa11b5f1401d4d176699204b0cce22aa Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 9 Nov 2024 20:57:09 +0100 Subject: [PATCH] add basic spectrum function --- packages/draw/draw.mjs | 2 +- packages/superdough/superdough.mjs | 3 +- packages/webaudio/index.mjs | 1 + packages/webaudio/spectrum.mjs | 57 ++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 packages/webaudio/spectrum.mjs diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index e3737600..0576c297 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -26,7 +26,7 @@ export const getDrawContext = (id = 'test-canvas', options) => { }, 200); }); } - return canvas.getContext(contextType); + return canvas.getContext(contextType, { willReadFrequently: true }); }; let animationFrames = {}; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 7d57b5bb..b61f4263 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -270,11 +270,12 @@ function getReverb(orbit, duration, fade, lp, dim, ir) { export let analysers = {}, analysersData = {}; -export function getAnalyserById(id, fftSize = 1024) { +export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) { if (!analysers[id]) { // make sure this doesn't happen too often as it piles up garbage const analyserNode = getAudioContext().createAnalyser(); analyserNode.fftSize = fftSize; + analyserNode.smoothingTimeConstant = smoothingTimeConstant; // getDestination().connect(analyserNode); analysers[id] = analyserNode; analysersData[id] = new Float32Array(analysers[id].frequencyBinCount); diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index a425e683..59672b61 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -6,4 +6,5 @@ This program is free software: you can redistribute it and/or modify it under th export * from './webaudio.mjs'; export * from './scope.mjs'; +export * from './spectrum.mjs'; export * from 'superdough'; diff --git a/packages/webaudio/spectrum.mjs b/packages/webaudio/spectrum.mjs new file mode 100644 index 00000000..1b3185ce --- /dev/null +++ b/packages/webaudio/spectrum.mjs @@ -0,0 +1,57 @@ +import { Pattern, clamp } from '@strudel/core'; +import { getDrawContext, getTheme } from '@strudel/draw'; +import { analysers, getAnalyzerData } from 'superdough'; + +/** + * Renders a spectrum analyzer for the incoming audio signal. + * @name spectrum + * @param {object} config optional config with options: + */ +let latestColor = {}; +Pattern.prototype.spectrum = function (config = {}) { + let id = config.id ?? 1; + return this.analyze(id).draw( + (haps) => { + config.color = haps[0]?.value?.color || latestColor[id] || getTheme().foreground; + latestColor[id] = config.color; + drawSpectrum(analysers[id], config); + }, + { id }, + ); +}; + +Pattern.prototype.scope = Pattern.prototype.tscope; + +const lastFrames = new Map(); + +function drawSpectrum( + analyser, + { thickness = 3, speed = 1, min = -80, max = 0, ctx = getDrawContext(), id = 1, color } = {}, +) { + ctx.lineWidth = thickness; + ctx.strokeStyle = color; + + if (!analyser) { + // if analyser is undefined, draw straight line + // it may be undefined when no sound has been played yet + return; + } + const scrollSize = speed; + const dataArray = getAnalyzerData('frequency', id); + const canvas = ctx.canvas; + ctx.fillStyle = color; + const bufferSize = analyser.frequencyBinCount; + let imageData = lastFrames.get(id) || ctx.getImageData(0, 0, canvas.width, canvas.height); + lastFrames.set(id, imageData); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + ctx.putImageData(imageData, -scrollSize, 0); + let q = canvas.width - speed; + for (let i = 0; i < bufferSize; i++) { + const normalized = clamp((dataArray[i] - min) / (max - min), 0, 1); + ctx.globalAlpha = normalized; + const next = (Math.log(i + 1) / Math.log(bufferSize)) * canvas.height; + const size = 2; //next - pos; + ctx.fillRect(q, canvas.height - next, scrollSize, size); + } + lastFrames.set(id, ctx.getImageData(0, 0, canvas.width, canvas.height)); +}