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

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

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

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

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

<iframe width="560" height="315" src="https://www.youtube.com/embed/dbwkVgXYYw8?si=isAE7x2HrfK0rCjr" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

### 情報を見かけたところ
具体的には、以下のページに出てきていたものです。

https://github.com/goldfire/howler.js/tree/master/examples/player

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/816c25e3-dfa4-3a6f-ad45-a05e5326b368.png)

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

https://x.com/0310lan/status/1339002761560555521

## SiriWave の情報を見てみる
それでは SiriWave を試してみます。

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/500a6418-1246-4ced-17a6-563f5febf742.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/05f9ad6f-7cf6-82a8-2266-45f43747c75a.png)

### 基本的な使い方の情報
描画領域を設定する方法が、以下の部分に書いてあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/b2ba2213-a42b-b490-4ceb-7a0e73bd7a74.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/34a93d27-966a-4615-5355-45ac757dc30b.png)

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

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

### マイク入力と連動するデモ
さらに一番下まで進むと、デモのリンクが書かれています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/4b7f0529-a1a6-a367-1cac-55b1382a606b.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/b775a5da-7a4f-9bd8-a90b-7c230e2cacea.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 のそれぞれの内容です。

```javascript
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();
```

```html
<!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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/5764d818-d81e-12f7-86f9-ee582e3fa238.png)

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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/345438d2-29a5-59a6-f447-c5a7dc77dc73.png)

```html
<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>
```

```javascript
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,
  });
};
```

```css
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 は、元の内容に、以下のライブラリの読み込みを足すだけにします。

<script src="https://unpkg.com/siriwave/dist/siriwave.umd.min.js"></script>

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

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

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

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

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

https://x.com/youtoy/status/1805953902388646118