もどる TOP

[JavaScript] Web Audio APIでボリューム・パン・フィルタいじるサンプル

 ブラウザゲーに音つけたいからWeb Audio APIいじりはじめたョー

 基本はAudioContextでノードを作って、ノード同士を繋げていく感じやったョ。繋げていく感じを掴むために、ここで作ったサンプルでは、トラック音量とマスター音量を用意してみたやよ。各トラック内でLowPassフィルター→パンナー→Gainというふうに繋げて、それらをさらにマスタートラックに繋げたョ。

 ざっくりした使い方は以下の前者のサイトを、フィルターとかパンナーとか細かいところは後者のサイトを参考にしたョ。やっぱりg200kgさんは神やよ。

Getting Started with Web Audio API - HTML5 Rocks

Web Audio API 解説 - 01.前説 | g200kg Music & Software

 そういやcreateStereoPannerの存在にきづかなくて、左右に音を振りたいだけなのに、ずいぶんcreatePannerに振り回された。と思いきや、iPhoneのSafariでcreateStereoPannerがサポートされてなくて、やはりcreatePannerに戻らなければならなかった。

 PannerNodesetPosition(x, y, z)zをゼロにすると (listenerzと近すぎる値にすると?) 左右の振りが極端になってしまうらしいので、適当に1をセットした。

 あと、音がいかにもピロピロしてるけど、スクリプト内でオシレータ使って作ってるわけじゃない。ややこしくてすいません。そういう音です。

サンプルページ

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8" />
	<meta name="robots" content="noarchive" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
	<meta name="landscape" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
	<script src="script.js"></script>
	<style>
* {
	margin: 0;
	padding: 0;
}
html {
    background-color:#000;
    color:#eee;
}
button {
	padding: 2px 4px;
}
input[type="range"] {
	margin: 0 4px;
}
.track {
	margin: 4px 0;
	padding: 5px;
	max-width: 500px;
	border: 1px solid #999;
	border-radius: 3px;
	background-color: #222;
	line-height: 1.3;
}
.track-name {
	margin-right: 5px;
	color: #ff9;
	font-weight: bold;
}
.slider {
	display: flex;
	align-items: center;
}
.slider dt {
	width: 3em;
}
	</style>
	<title>Web Audio APIサンプル</title>
</head>
<body>
	<div class="track">
		<span class="track-name">Master</span>
		<dl class="slider">
			<dt>Vol</dt>
			<dd>0<input type="range" min="0" max="1" step="0.01" value="0.8" id="volume_master" />1</dd>
		</dl>
	</div>
	<div class="track">
		<select id="select-audio_1"></select><button type="button" class="play-button" id="play_1">play</button>
		<dl class="slider">
			<dt>Vol</dt>
			<dd>0<input type="range" min="0" max="1" step="0.01" value="0.8" id="volume_1" />1</dd>
		</dl>
		<dl class="slider">
			<dt>Pan</dt>
			<dd>L<input type="range" min="-1" max="1" step="0.01" value="0" id="pan_1" />R</dd>
		</dl>
		<dl class="slider">
			<dt>LP</dt>
			<dd>0<input type="range" min="0" max="20000" step="10" value="20000" id="lp_1" />20000</dd>
		</dl>
	</div>
	<div class="track">
			<select id="select-audio_2"></select><button type="button" class="play-button" id="play_2">play</button>
		<dl class="slider">
			<dt>Vol</dt>
			<dd>0<input type="range" min="0" max="1" step="0.01" value="0.8" id="volume_2" />1</dd>
		</dl>
		<dl class="slider">
			<dt>Pan</dt>
			<dd>L<input type="range" min="-1" max="1" step="0.01" value="0" id="pan_2" />R</dd>
		</dl>
		<dl class="slider">
			<dt>LP</dt>
			<dd>0<input type="range" min="0" max="20000" step="10" value="20000" id="lp_2" />20000</dd>
		</dl>
	</div>
</body>
</html>
script.js
var _loadingNum = 0;
var _ctx;
var _sounds = {};
var _tracks = {};

// 初期化...
window.addEventListener('load', function() {
	// ブラウザによって異なるAudioContextをまとめる
	window.AudioContext = window.AudioContext || window.webkitAudioContext;
	if (!window.AudioContext) {
		alert('AudioContextに対応していないブラウザです。');
		return;
	}
	_ctx = new AudioContext();
	_ctx.listener.setPosition(0, 0, 0);

	// 音声をロードして…
	loadSamples(['a.wav', 'b.wav', 'c.wav', 'd.wav'], function() {
		// 完了したらセレクトボックス用のリストを作る
		var options = '';
		for (var name in _sounds) {
			options += '<option calue="'+ name +'">'+ name +'</option>';
		}
		document.getElementById('select-audio_1').innerHTML = options;
		document.getElementById('select-audio_2').innerHTML = options;

		// トラック準備
		// 1と2はMasterの末端ノードにつなぐ
		_tracks['master'] = new Track(_ctx.destination);
		_tracks['1'] = new Track(_tracks['master'].getTerminal());
		_tracks['1'].assign(_sounds['a.wav']);
		_tracks['2'] = new Track(_tracks['master'].getTerminal());
		_tracks['2'].assign(_sounds['a.wav']);

		// 操作ボタンなど準備
		setupTrackControl('master');
		setupTrackControl('1');
		setupTrackControl('2');
	});

	document.getElementById('nnn').addEventListener('click', function() {
		_tracks['1'].play();
	});
});

/**
 * トラックの操作ボタンなどの準備
 * @param trackName 
 */
function setupTrackControl(trackName) {
	var elem;
	if (elem = document.getElementById('play_'+ trackName)) {
		elem.addEventListener('click', function(e) {
			_tracks[trackName].play();
		});
	}
	if (elem = document.getElementById('select-audio_'+ trackName)) {
		elem.addEventListener('change', function(e) {
			_tracks[trackName].assign(_sounds[e.target.value]);
		});
	}
	if (elem = document.getElementById('volume_'+ trackName)) {
		elem.addEventListener('change', function(e) {
			_tracks[trackName].setVolume(e.target.value);
		});
	}
	if (elem = document.getElementById('pan_'+ trackName)) {
		elem.addEventListener('change', function(e) {
			_tracks[trackName].setPan(e.target.value);
		});
	}
	if (elem = document.getElementById('lp_'+ trackName)) {
		elem.addEventListener('change', function(e) {
			_tracks[trackName].setLowPassFilterFrequency(e.target.value);
		});
	}
}

/**
 * 音声ファイルを取得してデコードする
 * @param pathList 
 */
function loadSamples(pathList, callback) {
	_loadingNum += pathList.length;
	for (var i = 0; i < pathList.length; ++i) {
		var path = pathList[i];
		_sounds[path] = null;
		load(path, callback);
	}

	function load(path) {
		// 音声ファイルなのでレスポンスはarraybufferで受け取る
		var req = new XMLHttpRequest();
		req.open('GET', path, true);
		req.responseType = 'arraybuffer';
		req.addEventListener('load', function() {
			// 読み込み完了したら、デコードしてとっておく
			_ctx.decodeAudioData(req.response, function(buffer) {
				_sounds[path] = buffer;
				if (--_loadingNum <= 0) {
					// 全てデコード完了したら、コールバック関数を実行する
					callback();
				}
			}, function() {
				alert(path +'のデコードに失敗しました。');
			});
		});
		req.send();
	}
}

/**
 * トラッククラス
 * @param destination 出力先ノード
 * @param sound 音声バッファ
 */
function Track(destination) {
	// filter -> panner -> gain の順につなげる
	this.gain = _ctx.createGain();
	this.gain.connect(destination);
	this.panner = _ctx.createPanner();
	this.panner.connect(this.gain);
	_ctx.listener.setPosition(0, 0, 0);
	this.filter = _ctx.createBiquadFilter();
	this.filter.connect(this.panner);
	this.filter.type = 'lowpass';
	this.filter.frequency.value = 20000;
	this.sound = null;
	this.source = null;

	this.setVolume(0.7);
	this.setPan(0);
}
Track.prototype.assign = function(sound) {
	this.sound = sound;
};
Track.prototype.getTerminal = function() {
	// 終端のノード (ここではfilter) を返却 
	return this.filter;
};
Track.prototype.setVolume = function(value) {
	this.gain.gain.value = value;
};
Track.prototype.setPan = function(value) {
	this.panner.setPosition(value, 0, 1); // (x, y, z) だが、zが0だとlistenerに近すぎて左右に振られすぎるらしい
};
Track.prototype.setLowPassFilterFrequency = function(value) {
	this.filter.frequency.value = value;
};
Track.prototype.play = function() {
	if (!this.sound) {
		return;
	}
	if (this.source) {
		// 再生中なら止める
		this.source.stop();
	}
	this.source = _ctx.createBufferSource();
	this.source.buffer = this.sound;
	this.source.connect(this.getTerminal()); // 終端のノードにつないで再生
	this.source.start(0);
}
サイドバーを表示する
ブログ
ShortCircuit
ShortCircuit
花火大会
天使
去る512時間前、キリウ君は折れてない千歳飴を渡してきて、ぼくが折るよう仕向けた。1024時間前、彼はこの世のものではないハッシュアルゴリズムでひとりブロックチェーンを始めていた。