LoginSignup
1
0
この記事誰得? 私しか得しないニッチな技術で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

Siri風の波形アニメーションを描画できるライブラリ「SiriWave」を p5.js Web Editor上で軽く試してみる

Last updated at Posted at 2024-06-26

はじめに

直近の記事で題材にしていた howler.js のサンプルを見ていたとき、その中に登場していた「SiriWave」が気になって、それで試してみたという内容です。

●howler.js の「Audio Sprites」を活用して音声ファイルの特定の部分を指定して使う - Qiita
 https://qiita.com/youtoy/items/f2899bf34707165e1353

これは、どこかで見たような感じがする波形を、簡単に描画するためのライブラリです。

今回試した内容

今回試した内容を動画にしたものを、まずは掲載してみます。

情報を見かけたところ

具体的には、以下のページに出てきていたものです。

image.png

X でずいぶん前にポストされてた話

ちなみに、X でポストされた情報があったりするかと思って検索をしてみたところ、以下のポストが出てきました。

SiriWave の情報を見てみる

それでは SiriWave を試してみます。

●kopiro/siriwave: The Apple® Siri wave-form replicated in a JS library.
 https://github.com/kopiro/siriwave

image.png

ライブラリの読み込み

ライブラリの準備については、以下の CDN からの読み込みが簡単そうです。

image.png

基本的な使い方の情報

描画領域を設定する方法が、以下の部分に書いてあります。

image.png

また、その下にオプションが書いてあります。

image.png

必須のものは「container」のみで、他は任意となるもののようです。

その下に、API の説明などが書いてあります。

マイク入力と連動するデモ

さらに一番下まで進むと、デモのリンクが書かれています。

image.png

デモは以下のような内容で、マイクから入力された音に反応して波を描画する、というもののようです。

image.png

JavaScript の内容を見た感じでは、「Web Audio API」を使ってマイク入力を取得しているようです。そして以下の部分で、マイクからの入力を描画した波の振幅に反映させているようでした。

      // find the maximum not considering negative values (without loss of generality)
      const amplitude = waveForm.reduce((acc, y) => Math.max(acc, y), 128) - 128;

      //scale amplituded from [0, 128] to [0, 10].
      siriWave.setAmplitude(amplitude / 128 * 10);

以下が JavaScript・HTML のそれぞれの内容です。

const siriWave = new SiriWave({
  container: document.querySelector('#visualiser'),
  cover: true, // means the visualisation scales *responsively* according to the element's dimensions
  height: 400,
  style: "ios9"
});

let source = undefined;
let taskHandle = 0;
let spectrum, dBASpectrum;

const 
  energyElement = document.querySelector('#energy'),
  frequencyElement = document.querySelector('#frequency'),
  meanFrequencyElement = document.querySelector('#meanFrequency'),
  maxPowerElement = document.querySelector('#maxPowerFrequency'),
  
  // A-weighting
  // https://www.softdb.com/difference-between-db-dba/
  // https://en.wikipedia.org/wiki/A-weighting
  RA = f => 
    12194 ** 2 * f ** 4 /
    ((f ** 2 + 20.6 ** 2) * Math.sqrt((f ** 2 + 107.7 ** 2) * (f ** 2 + 737.9 ** 2)) * (f ** 2 + 12194 ** 2)),
  A = f => 20 * Math.log10(RA(f)) + 2.0;

// see https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API

function run() {
  const audioStream =
    navigator.mediaDevices.getUserMedia({ audio: true, video: false });

  // Note that the visualisation itself is animated with fps_ani = 60 Hz ↷ interval_ani = 17 msec
  // ν
  const approxVisualisationUpdateFrequency = 5;
  // total sample time T = 1 / ν
  // sampling rate f
  // total number of samples N = f ∙ T

  audioStream
  .then(stream => Promise.all([stream, navigator.mediaDevices.enumerateDevices()]))
  .then(([stream, devices]) => {
    //context depending on browser(Chrome/Firefox)
    let context = new (window.AudioContext || window.webkitAudioContext)();
    //create source for sound input.
    source = context.createMediaStreamSource(stream);
    //create analyser node.
    let analyser = context.createAnalyser();

    const 
      trackSettings = stream.getAudioTracks()[0].getSettings(),
      sampleRate = trackSettings.sampleRate || context.sampleRate, // Firefox does not support trackSettings.sampleRate
      deviceName = devices.find(device => device.deviceId === trackSettings.deviceId).label;

    console.log(`sample rate: ${sampleRate} Hz, 
    audio context sample rate: ${context.sampleRate} Hz,
    dynamic: ${trackSettings.sampleSize} bit
    device: ${deviceName}`);

    let totalNumberOfSamples = 
      sampleRate / approxVisualisationUpdateFrequency; // e.g. 48000 / 5 = 9600

    analyser.fftSize = 2 ** Math.floor(Math.log2(totalNumberOfSamples));

    const 
      uint8TodB = byteLevel => 
        (byteLevel / 255) * (analyser.maxDecibels - analyser.minDecibels) + analyser.minDecibels;

    console.log(`frequency bins: ${analyser.frequencyBinCount}`);

    const
      weightings = [-100];
    for (let i = 1; i < analyser.frequencyBinCount; i++) {
      weightings[i] = A(i * sampleRate / 2 / analyser.frequencyBinCount);
    }

    //array for frequency data.
    // holds Number.NEGATIVE_INFINITY, [0 = -100dB, ..., 255 = -30 dB]
    spectrum = new Uint8Array(analyser.frequencyBinCount);
    dBASpectrum = new Float32Array(analyser.frequencyBinCount);

    let waveForm = new Uint8Array(analyser.frequencyBinCount);

    //connect source->analyser->destination.
    source.connect(analyser);
    // noisy feedback loop if we put the mic on the speakers 
    //analyser.connect(context.destination);

    siriWave.start();

    const updateAnimation = function (idleDeadline) {
      taskHandle = requestIdleCallback(updateAnimation, { timeout: 1000 / approxVisualisationUpdateFrequency });

      //copy frequency data to spectrum from analyser.
      // holds Number.NEGATIVE_INFINITY, [0 = -100dB, ..., 255 = -30 dB]
      analyser.getByteFrequencyData(spectrum);

      spectrum.forEach((byteLevel, idx) => {
        dBASpectrum[idx] = uint8TodB(byteLevel) + weightings[idx];
      });

      const 
        highestPerceptibleFrequencyBin =
          dBASpectrum.reduce((acc, y, idx) => y > -90 ? idx : acc, 0),
        // S = ∑ s_i
        totaldBAPower =
          dBASpectrum.reduce((acc, y) => acc + y),

        // s⍉ = ∑ s_i ∙ i / ∑ s_i
        meanFrequencyBin =
          dBASpectrum.reduce((acc, y, idx) => acc + y * idx) / totaldBAPower,

        highestPowerBin = 
          dBASpectrum.reduce(([maxPower, iMax], y, idx) => 
            y > maxPower ? [y, idx] : [maxPower, iMax], [-120, 0]
          )[1],
          
        highestDetectedFrequency = 
          highestPerceptibleFrequencyBin * (sampleRate / 2 / analyser.frequencyBinCount),
        meanFrequency = 
          meanFrequencyBin * (sampleRate / 2 / analyser.frequencyBinCount),
        maxPowerFrequency = 
          highestPowerBin * (sampleRate / 2 / analyser.frequencyBinCount);

      //set the speed for siriwave
      // scaled to [0..22kHz] -> [0..1]
      siriWave.setSpeed(maxPowerFrequency / 10e+3);
      
      const averagedBAPower = 
        totaldBAPower / analyser.frequencyBinCount;

      // for fun use raf to update the screen
      requestAnimationFrame(() => {
        energyElement.textContent = averagedBAPower.toFixed(2);
        frequencyElement.textContent = highestDetectedFrequency.toFixed(0);
        meanFrequencyElement.textContent = meanFrequency.toFixed(0);
        maxPowerElement.textContent = maxPowerFrequency.toFixed(0);
      });

      //find the max amplituded
      // the zero level is at 128
      analyser.getByteTimeDomainData(waveForm);

      // find the maximum not considering negative values (without loss of generality)
      const amplitude = waveForm.reduce((acc, y) => Math.max(acc, y), 128) - 128;

      //scale amplituded from [0, 128] to [0, 10].
      siriWave.setAmplitude(amplitude / 128 * 10);
    };

    taskHandle = requestIdleCallback(updateAnimation, { timeout: 1000 / approxVisualisationUpdateFrequency });
  });
}

function stop() {
  cancelIdleCallback(taskHandle);
  siriWave.setAmplitude(0);
  siriWave.setSpeed(0);
  source.disconnect();
  siriWave.stop();
  source.mediaStream.getAudioTracks()[0].stop();
}

document.querySelector('#stop-button').addEventListener('click', stop);

run();
<!doctype html>
<html>

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1" />
	<meta name="referrer" content="no-referrer" />
	<meta name="referrer" content="never" />
	<link rel="shortcut icon"
		href="data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' %3E%3Cpath d='M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z' /%3E%3Cpath d='M19 10v2a7 7 0 0 1-14 0v-2' /%3E%3Cline x1='12' y1='19' x2='12' y2='23' /%3E%3Cline x1='8' y1='23' x2='16' y2='23' /%3E%3C/svg%3E" />
	<title>Modern Analyser Demo</title>
	<style type="text/css">
		:root {
			color-scheme: light dark;
		}

		/* see https://github.com/angular/material/issues/681 */
		:root,
		body {
			position: fixed;
			top: 0;
			bottom: 0;
			left: 0;
			right: 0;
			margin: 0;
			padding: 0;
			height: 100%;
		}

		body {
			font-family: sans-serif;
			background-color: lightblue;
		}

		#visualiser {
			border: solid 1px blue;
			background-color: #00000080;
		}
	</style>
</head>
<body>
	<div id="visualiser"></div>
	<div>Power = <span id="energy"></span> dB(A)</div>
  <div>f<sub>max</sub> = <span id="frequency"></span> Hz</div>
  <div>f<sub>mean</sub> = <span id="meanFrequency"></span> Hz</div>
  <div>f<sub>max power</sub> = <span id="maxPowerFrequency"></span> Hz</div> 
	<button type="button" id="stop-button">Stop</button>
  <script src="https://cdn.jsdelivr.net/gh/kopiro/siriwave/dist/siriwave.umd.js"></script>
</body>
</html>

描画に関する説明

また、GitHub のリポジトリの冒頭のところに、描画に関する説明が書いてあるブログ記事へのリンクが掲載されています。

●How I built the SiriWaveJS library: a look at the math and the code - DEV Community
 https://dev.to/kopiro/how-i-built-the-siriwavejs-library-a-look-at-the-math-and-the-code-l0o

image.png

波を描画する話の詳細が気になった場合は、こちらを見てみると良さそうです。

シンプルなデモ

上で 1つデモを紹介しましたが、それよりシンプルなデモも掲載されています。

●Siriwave example
 https://codepen.io/kopiro/pen/oNYepEb

image.png

<script src="https://unpkg.com/siriwave/dist/siriwave.umd.min.js"></script>
  
<h3>Classic</h3>
<div id="container"></div>

<h3>iOS9+</h3>
<div id="container-9"></div>
window.onload = function() {
  const SW = new SiriWave({
  container: document.getElementById("container"),
  autostart: true,
  });

  const SW9 = new SiriWave({
  style: "ios9",
  container: document.getElementById("container-9"),
  autostart: true,
  });
};
body {
  margin: 0;
  background: #000;
  color: #fff;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
    "Helvetica Neue", sans-serif;
  text-align: center;
}

a {
  color: #fff;
}

#container,
#container-9 {
  width: 600px;
  height: 300px;
  background-size: cover;
  margin: 20px;
  margin: 0 auto;
  border: 1px dashed rgba(255, 255, 255, 0.4);
}

とりあえず試す場合は、上記のシンプルなデモを元にするのが良さそうです。

どうやら、コンテナにする要素を指定してやれば、パラメータ設定なしでも描画が行えるようです。

p5.js Web Editor上で試す

とりあえず、シンプルな内容で自分でも試してみます。
環境は p5.js Web Editor を使うことにしました

実装内容

実装は、以下のような内容にしてみました。

HTML

HTML は、元の内容に、以下のライブラリの読み込みを足すだけにします。

CSS のファイルには手を加えず、JavaScript の中で CSS に関する処理を書いてみます。

JavaScript

JavaScript の実装は以下のとおりです。

function setup() {
  select("html").style("background", "black");

  const SW = new SiriWave({
    container: document.body,
    width: 700,
    height: 500,
    style: "ios9",
    autostart: true,
  });
}

これを実行すると、冒頭にも掲載していた内容を描画できます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0