1import { Pattern, clamp } from '@strudel/core'; 2import { getDrawContext, getTheme } from '@strudel/draw'; 3import { analysers, getAnalyzerData } from 'superdough'; 4 5/** 6 * Renders a spectrum analyzer for the incoming audio signal. 7 * @name spectrum 8 * @tags visualization 9 * @param {object} config optional config with options: 10 * @param {integer} thickness line thickness in px (default 3) 11 * @param {integer} speed scroll speed (default 1) 12 * @param {integer} min min db (default -80) 13 * @param {integer} max max db (default 0) 14 * @example 15 * n("<0 4 <2 3> 1>*3") 16 * .off(1/8, add(n(5))) 17 * .off(1/5, add(n(7))) 18 * .scale("d3:minor:pentatonic") 19 * .s('sine') 20 * .dec(.3).room(.5) 21 * ._spectrum() 22 */ 23let latestColor = {}; 24Pattern.prototype.spectrum = function (config = {}) { 25 let id = config.id ?? 1; 26 return this.analyze(id).draw( 27 (haps) => { 28 config.color = haps[0]?.value?.color || latestColor[id] || getTheme().foreground; 29 latestColor[id] = config.color; 30 drawSpectrum(analysers[id], config); 31 }, 32 { id }, 33 ); 34}; 35 36Pattern.prototype.scope = Pattern.prototype.tscope; 37 38const lastFrames = new Map(); 39 40function drawSpectrum( 41 analyser, 42 { thickness = 3, speed = 1, min = -80, max = 0, ctx = getDrawContext(), id = 1, color } = {}, 43) { 44 ctx.lineWidth = thickness; 45 ctx.strokeStyle = color; 46 47 if (!analyser) { 48 // if analyser is undefined, draw straight line 49 // it may be undefined when no sound has been played yet 50 return; 51 } 52 const scrollSize = speed; 53 const dataArray = getAnalyzerData('frequency', id); 54 const canvas = ctx.canvas; 55 ctx.fillStyle = color; 56 const bufferSize = analyser.frequencyBinCount; 57 let imageData = lastFrames.get(id) || ctx.getImageData(0, 0, canvas.width, canvas.height); 58 lastFrames.set(id, imageData); 59 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 60 ctx.putImageData(imageData, -scrollSize, 0); 61 let q = canvas.width - speed; 62 for (let i = 0; i < bufferSize; i++) { 63 const normalized = clamp((dataArray[i] - min) / (max - min), 0, 1); 64 ctx.globalAlpha = normalized; 65 const next = (Math.log(i + 1) / Math.log(bufferSize)) * canvas.height; 66 const size = 2; //next - pos; 67 ctx.fillRect(q, canvas.height - next, scrollSize, size); 68 } 69 lastFrames.set(id, ctx.getImageData(0, 0, canvas.width, canvas.height)); 70}