Connor McCutcheon
/ Music
scope.mjs
mjs
import { Pattern, clamp } from '@strudel/core';
import { getDrawContext, getTheme } from '@strudel/draw';
import { analysers, getAnalyzerData } from 'superdough';
export function drawTimeScope(
  analyser,
  {
    align = true,
    color = 'white',
    thickness = 3,
    scale = 0.25,
    pos = 0.75,
    trigger = 0,
    ctx = getDrawContext(),
    id = 1,
  } = {},
) {
  ctx.lineWidth = thickness;
  ctx.strokeStyle = color;
  let canvas = ctx.canvas;
  if (!analyser) {
    // if analyser is undefined, draw straight line
    // it may be undefined when no sound has been played yet
    ctx.beginPath();
    let y = pos * canvas.height;
    ctx.moveTo(0, y);
    ctx.lineTo(canvas.width, y);
    ctx.stroke();
    return;
  }
  const dataArray = getAnalyzerData('time', id);
  ctx.beginPath();
  const bufferSize = analyser.frequencyBinCount;
  let triggerIndex = align
    ? Array.from(dataArray).findIndex((v, i, arr) => i && arr[i - 1] > -trigger && v <= -trigger)
    : 0;
  triggerIndex = Math.max(triggerIndex, 0); // fallback to 0 when no trigger is found
  const sliceWidth = (canvas.width * 1.0) / bufferSize;
  let x = 0;
  for (let i = triggerIndex; i < bufferSize; i++) {
    const v = dataArray[i] + 1;
    const y = (pos - scale * (v - 1)) * canvas.height;
    if (i === 0) {
      ctx.moveTo(x, y);
    } else {
      ctx.lineTo(x, y);
    }
    x += sliceWidth;
  }
  ctx.stroke();
}
export function drawFrequencyScope(
  analyser,
  { color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0, ctx = getDrawContext(), id = 1 } = {},
) {
  if (!analyser) {
    ctx.beginPath();
    let y = pos * canvas.height;
    ctx.moveTo(0, y);
    ctx.lineTo(canvas.width, y);
    ctx.stroke();
    return;
  }
  const dataArray = getAnalyzerData('frequency', id);
  const canvas = ctx.canvas;
  ctx.fillStyle = color;
  const bufferSize = analyser.frequencyBinCount;
  const sliceWidth = (canvas.width * 1.0) / bufferSize;
  let x = 0;
  for (let i = 0; i < bufferSize; i++) {
    const normalized = clamp((dataArray[i] - min) / (max - min), 0, 1);
    const v = normalized * scale;
    const h = v * canvas.height;
    const y = (pos - v * lean) * canvas.height;
    ctx.fillRect(x, y, Math.max(sliceWidth, 1), h);
    x += sliceWidth;
  }
}
function clearScreen(smear = 0, smearRGB = `0,0,0`, ctx = getDrawContext()) {
  if (!smear) {
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  } else {
    ctx.fillStyle = `rgba(${smearRGB},${1 - smear})`;
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  }
}
/**
 * Renders an oscilloscope for the frequency domain of the audio signal.
 * @name fscope
 * @param {string} color line color as hex or color name. defaults to white.
 * @param {number} scale scales the y-axis. Defaults to 0.25
 * @param {number} pos y-position relative to screen height. 0 = top, 1 = bottom of screen
 * @param {number} lean y-axis alignment where 0 = top and 1 = bottom
 * @param {number} min min value
 * @param {number} max max value
 * @example
 * s("sawtooth").fscope()
 */
Pattern.prototype.fscope = function (config = {}) {
  let id = config.id ?? 1;
  return this.analyze(id).draw(
    () => {
      clearScreen(config.smear, '0,0,0', config.ctx);
      analysers[id] && drawFrequencyScope(analysers[id], config);
    },
    { id },
  );
};
/**
 * Renders an oscilloscope for the time domain of the audio signal.
 * @name scope
 * @synonyms tscope
 * @param {object} config optional config with options:
 * @param {boolean} align if 1, the scope will be aligned to the first zero crossing. defaults to 1
 * @param {string} color line color as hex or color name. defaults to white.
 * @param {number} thickness line thickness. defaults to 3
 * @param {number} scale scales the y-axis. Defaults to 0.25
 * @param {number} pos y-position relative to screen height. 0 = top, 1 = bottom of screen
 * @param {number} trigger amplitude value that is used to align the scope. defaults to 0.
 * @example
 * s("sawtooth")._scope()
 */
let latestColor = {};
Pattern.prototype.tscope = function (config = {}) {
  let id = config.id ?? 1;
  return this.analyze(id).draw(
    (haps) => {
      config.color = haps[0]?.value?.color || getTheme().foreground;
      latestColor[id] = config.color;
      clearScreen(config.smear, '0,0,0', config.ctx);
      drawTimeScope(analysers[id], config);
    },
    { id },
  );
};
Pattern.prototype.scope = Pattern.prototype.tscope;
No comments yet.