Web audio API を使ってChrome拡張機能をつくってみた

10:22・2021/08/09 公開

02:31・2021/08/16 更新

突然ですが、あなたはにじさんじの新人(2021夏時点)ライバーの「レオス・ヴィンセント」をご存じでしょうか?
彼の配信がクッソ面白いので、もっと面白くしようと思ってChrome拡張機能を作りました。

その名も「LEOS_meter(レオスメーター)」。

※名づけのセンスが無い

概要

「LEOS_meter」はYouTubeで動画を開いたときに、動画の上部に「Web audio API」で取得した音量(正しくは音域毎のgainを足した数値)を表示する拡張機能です。

画面はこんな感じ。

kawaii
澄ました顔で1000Gain超えるレオス

左側にマッドサイエンティスのレオスの研究対象である「まめねこ」を表示、右側に音量を表示しています。
(本当はいい感じにスケーリングしてdBで表示したかった)。

音が止まっているときまめねこは寝ていますが、音が出ているときはまめねこの「まめ」の部分(?)が動きます。

レオスが大きい声を出すと双葉がブンブンします。(これがやりたかった)

使い方

ダウンロード

コード配布用 – Googleドライブ

LEOS_meter – GitHub

お好きな方をどうぞ。

拡張機能の読み込み

「chrome://extensions/」にアクセスし、右上のデベロッパーモードをオン。

「パッケージ化されていない拡張機能を読み込む」から、解凍したzipファイルを読み込むことで使えます。

本当はストアで公開したかったんですが、二次創作ガイドライン的にどうなんだろうとか、ストアの開設にお金がかかるとかで諦めました。
気に入ったら拡散していただけると嬉しいです。

実装について

そんな大層なコードでもないので全文公開します。
Web audio API に慣れるのに3日かかりました。全然理解できん。

manifest.json

{
  "name": "LEOS_meter",
  "version": "1.0",
  "description": "YouTubeオーディオの再生中に音の大きさを表示します。/ Shows the loudness while playing YouTube audio.",
  "manifest_version": 2,
  "permissions": [
    "activeTab",
    "tabs"
  ],
  "content_scripts": [
    {
      "matches": [
        "https://www.youtube.com/*"
      ],
      "js": [
        "function.js"
      ]
    }
  ],
  "web_accessible_resources": [
    "images/*.GIF"
  ]
}

function.js

'use strict';

function add_container() {//youtube.comのHTMLに表示領域を追加
	const wrapper = document.getElementById('primary-inner');
	let meter = document.createElement('div');
	meter.id = 'LEOS_meter';
	meter.style.boxSizing = 'border-box';
	meter.style.width = '100%';
	meter.style.height = '48px';
	meter.style.padding = '0 20px';
	meter.style.backgroundColor = '#000';
	meter.style.border = '1px solid #454545';
	meter.style.display = 'flex';
	meter.style.justifyContent = 'space-between';
	meter.style.alignItems = 'center';
	let animation_box = document.createElement('div');
	let animation_gif = document.createElement('img');
	animation_gif.id = 'LEOS_gif';
	animation_gif.style.maxHeight = '46px';
	animation_gif.style.objectFit = 'contain';
	animation_gif.setAttribute('sec', chrome.runtime.getURL('images/mameneko.GIF'));
//拡張機能のフォルダにアクセスしてGIFを取得↑
	animation_box.appendChild(animation_gif);
	meter.appendChild(animation_box);
	let int_box = document.createElement('p');
	int_box.style.height = '48px';
	int_box.style.display = 'flex';
	int_box.style.alignItems = 'flex-end';
	let int = document.createElement('span');
	int.id = 'LEOS_int';
	int.style.color = '#454545';
	int.style.fontSize = '36px';
	let unit = document.createElement('span');
	unit.style.color = '#454545';
	// unit.textContent = 'dB';
	unit.textContent = 'Gain';
	unit.style.fontSize = '20px';
	unit.style.paddingBottom = '4px';
	int_box.appendChild(int); int_box.appendChild(unit);
	meter.appendChild(int_box);
	wrapper.insertBefore(meter, wrapper.firstChild);
}

function audio() {
	let gif_ctrl = document.getElementById('LEOS_gif');
	let int = document.getElementById('LEOS_int')
	const audioElement = document.getElementsByClassName('html5-main-video')[0];//videoタグ取得
	var audioCtx = new AudioContext();//オーディオの作成
	let source = audioCtx.createMediaElementSource(audioElement);//videoタグのオーディオを登録
	var analyser = audioCtx.createAnalyser();//アナライザの作成

	analyser.fftSize = 32; //音域の数
	var bufferLength = analyser.frequencyBinCount;
	var dataArray = new Uint8Array(bufferLength);
	analyser.getByteTimeDomainData(dataArray);

	var bufferLength = analyser.frequencyBinCount;

	//↓の配列に音域ごとの大きさが入る
	var dataArray = new Uint8Array(bufferLength);
//↓登録したオーディオをアナライザに流してから、スピーカーに出力
	source.connect(analyser).connect(audioCtx.destination);
	getAudio();


	function getAudio() {
		requestAnimationFrame(getAudio);//フレーム毎に繰り返し呼び出し
		analyser.getByteFrequencyData(dataArray);
		//音域の総和を得る
		var dNum = 0;
		for (var i = 0; i < dataArray.length; i++) {
			dNum += dataArray[i];
		}

///////////////////////////////////////////////////////////////////
//dBにスケーリングしようとした残骸
		// let inputY = (dNum / 100);
		// let xMax = 0;
		// let xMin = -30;

		// let yMax = 20;
		// let yMin = 0;

		// let percent = (inputY - yMin) / (yMax - yMin);
		// let outputX = percent * (xMax - xMin) + xMin;

		// int.textContent = Number.parseFloat(outputX).toFixed(2);
////////////////////////////////////////////////////////////////////////

		int.textContent = ((Math.floor(dNum / 10).toFixed(1)) * 10);//一の位が動き過ぎなので切り捨て

		if (dNum == 0) {//GIFのスイッチ
			if (!gif_ctrl.classList.contains('play')) {
				return;
			} else {
				gif_ctrl.setAttribute('class', '');
				gif_ctrl.setAttribute('src', chrome.runtime.getURL('images/mameneko.GIF'));
			}
		} else if (dNum <= 1000) {
			if (gif_ctrl.classList.contains('play')) {
				return;
			} else {
				gif_ctrl.setAttribute('class', '');
				gif_ctrl.classList.add('play');
				gif_ctrl.setAttribute('src', chrome.runtime.getURL('images/mameneko_play.GIF'));
			}
		} else {
			if (gif_ctrl.classList.contains('fast')) {
				return;
			} else {
				gif_ctrl.setAttribute('class', '');
				gif_ctrl.classList.add('fast');
				gif_ctrl.setAttribute('src', chrome.runtime.getURL('images/mameneko_fast.GIF'));
			}
		}
	}
}

window.addEventListener('load', () => {
	console.log('LEOS_meter is running.');
	setTimeout(() => {
		add_container(), audio()
	}, 500);//youtubeのscriptタグ実行のため0.5秒待つ
})

ディレクトリ構造

  • LEOS_meter
    • manifest.json
    • function.js
    • images
      • mameneko.GIF
      • mameneko_play.GIF
      • mameneko_fast.GIF

GIFデータ

↓まめねこのGIF単体はここからDLできます。
LEOS_meter_GIF – GoogleDrive

LEOS_meter以外での使用の際は、ここのコメント欄かTwitterにて必ず教えてください。
改変・自作発言禁止

参考サイト

【ドット絵制作】
ドット絵エディタ – Dottable
多機能でとても使いやすかった

【コーディング】
Web Audio API
MDNのマニュアル

Web AudioでYoutube、itunes上の曲を元に、WebGL(GLSLシェーダー)でリアルタイムビジュアライズ
APIについて

Chromeの拡張機能を作ってみる備忘録。
拡張機能の画像の読み込み方について

Twitterから「いいね」を消し去るChrome拡張を作る
JSONの参考用

2つの数値範囲間のスケーリング
数値のスケーリングについて

動画の適切な音量はどれくらい?音声・BGM・効果音の音量調整の目安をまとめてみた
スケーリングの参考用

自作のChrome Extensionをインポートした時に “Invalid value for ‘content_scripts[0].matches[0]’: Empty path.”というエラーが出たので解決した
エラーが出たので

WEB SOUNDER
すごく古いサイトだけどMDNよりわかりやすかった

補足

videoタグからリアルタイムにaudioデータを取る方法が検索してもわからなくて、コードをこねこねしていたら偶然成功しました。

※追記 R3/8/16

音量を良い感じにするオプションを付けたversion1.1をGoogleDriveに公開しました。

LEOS_meter_v1.1を読み込み後、右クリックでメニューが出ますので、オプションから設定を変更してください。
※初回起動時はまめねこも音量調整もOFFになっています。

実装について

【Among Us】エデン組、疑心暗鬼。【レオス・ヴィンセント/にじさんじ】
こちらの動画を参考に、主に1000gainを超える部分を削る感じで実装しました。

(僕は音でビビらせてくる動画は大嫌いなんですねぇ~~~)

//normalize
		if (val_gain) {
			if (dNum < 500) {
				volume = 1.1;
				gainNode.gain.value = volume;
			}
			if (dNum < 900) {
				volume = 1;
				gainNode.gain.value = volume;
			}
			if (dNum >= 1000) {
				volume = 0.4;
				gainNode.gain.value = volume;
			}
			if (dNum >= 1050) {
				volume = 0.3;
				gainNode.gain.value = volume;
			}
			if (dNum >= 1100) {
				volume = 0.1;
				gainNode.gain.value = volume;
			}
			if (dNum >= 1150) {
				volume = 0.001;
				gainNode.gain.value = volume;
			}
			if (dNum >= 1200) {
				volume = 0.00001;
				gainNode.gain.value = volume;
			}
			if (dNum >= 1300) {
				volume = 0.000001;
				gainNode.gain.value = volume;
			}
			if (dNum >= 1400) {
				volume = 0.0000001;
				gainNode.gain.value = volume;
			}
		}

追記部分こんなんです。
絶対もっといい実装があるはず。

この実装には難点がいくつかありまして、

  • 別タブで作業していると音量調整が効かない
  • gainを雑に下げているため音がガタガタする

といった感じです。

以上、解散。