jevstrudel.git / packages / webaudio / spectrum.mjs
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}