Skip to content

Instantly share code, notes, and snippets.

@uupaa
Created March 3, 2016 11:25
Show Gist options
  • Select an option

  • Save uupaa/e92fbd3643391806918f to your computer and use it in GitHub Desktop.

Select an option

Save uupaa/e92fbd3643391806918f to your computer and use it in GitHub Desktop.
WebAudio.js test
<button onclick="play()">play</button>
<script>
var bufsize = 4096 / 2;
var audioctx = new webkitAudioContext();
var scrproc = audioctx.createScriptProcessor(bufsize, 2);
scrproc.onaudioprocess = processHandler;
scrproc.connect(audioctx.destination);
var osc = null;
var currentTimeArray = [];
var timeStampArray = [];
var lastCurrentTime = 0;
var lastPlayBackTime = 0;
// 44.1kHz = 44100 = 1秒に44100フレームのサンプリングレート
// = 1フレーム辺り 0.00002267573696 秒
// * 4096フレーム = 0.09287981858816秒
// 4096 = 0.09287981859410431
// 44.1kHz で 2048フレーム毎にコールバックすると
// 呼び出し間隔は、 0.04643990929705216
function processHandler(event) {
var currentTime = audioctx.currentTime;
var playbackTime = event.playbackTime;
currentTimeArray.push(currentTime - lastCurrentTime);
timeStampArray.push(playbackTime - lastPlayBackTime);
lastCurrentTime = currentTime;
lastPlayBackTime = playbackTime;
var buf0 = event.outputBuffer.getChannelData(0);
for(var i = 0; i < bufsize; ++i) {
buf0[i] = (Math.random() * 2) - 1;
}
if (currentTimeArray.length > 100) {
scrproc.disconnect();
console.dir(currentTimeArray);
console.dir(timeStampArray);
}
}
function play() {
osc = audioctx.createOscillator();
osc.connect(scrproc);
osc.start(0);
}
play();
</script>
<button onclick="play()">play</button><br />
<button onclick="stop()">stop</button><br />
<input id="a" type="text" value="" /><br />
<input id="b" type="text" value="" /><br />
<input id="c" type="text" value="" /><br />
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
<script>
var bufferSize = 4096 / 2;
var audioContext = new webkitAudioContext();
var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1);
scriptProcessor.onaudioprocess = processHandler;
scriptProcessor.connect(audioContext.destination);
var oscillator = null;
var currentTimeArray = [];
var timeStampArray = [];
var lastCurrentTime = 0;
var lastPlayBackTime = 0;
var now = performance.now();
/*
var diffTime1 = (now / 1000) - audioContext.currentTime;
*/
var nodeA = document.querySelector("#a");
var nodeB = document.querySelector("#b");
var nodeC = document.querySelector("#c");
/*
## WebAudio の AudioContext.currentTime を用いたイベントキュー設計のポイント
WebAudio の ScriptProcessorNode を上手く使うと、
精度が高く安定したコールバックを定期的に発生させる事ができます。
この特性を利用すると、安定したリップシンク(音声と動画の同期処理)を実現する事が可能になります。
### 安定したリップシンクを実現するために
リップシンクを安定させるには、以下の条件を可能な限り満たす必要があります。
- できるだけ精度の高いクロックが必要です
- setTimeout, setInterval の最小分解能は 4ms です(仕様上の値で、実装次第です)
- requestAnimationFrame の最小分解能は 16.666ms です(iPhone 5, iOS 9 では約33ms になります)
- メインスレッドの混雑状況の影響を受けずに動作する仕組みが必要です
- setTimeout はメインスレッドの影響を強く受けます
- requestAnimationFrame はリフレッシュレートの影響を強く受けます
- AudioContext#currentTime は別スレッドで動作するためメインスレッドの影響を受けません
基本的な構造は、以下のようになります。
```js
var bufferSize = 2048;
var audioContext = new AudioContext();
var oscillatorNode = null;
var scriptProcessorNode = audioContext.createScriptProcessor(bufferSize, 1, 1);
scriptProcessorNode.onaudioprocess = processHandler;
scriptProcessorNode.connect(audioContext.destination);
function processHandler(event) {
var currentTime = audioContext.currentTime;
var inputBuffer0 = event.inputBuffer.getChannelData(0); // Float32Array
var outputBuffer0 = event.outputBuffer.getChannelData(0); // Float32Array
outputBuffer0.set(inputBuffer0, 0); // inputBuffer を outputBuffe にコピーする
// currentTime に応じたスケジューリング処理をここに記述する
}
function play() {
if (oscillatorNode) { return; }
oscillatorNode = audioContext.createOscillator();
oscillatorNode.type = "sine";
oscillatorNode.frequency.value = 440; // 440Hz
oscillatorNode.connect(scriptProcessorNode);
oscillatorNode.start();
}
```
この特性を使うと、
AudioContext.currentTime
設計の手戻りを避けるために、事前に明確にすべきポイント。
- performance.now() と AudioContext.currentTime のクロック特性を調査する
- 単位は?
- 精度は?
- 次第にズレが発生する?
- tab hidden 状態ではどうなる?
- 更新頻度が落ちてズレる事はないか?
- WebAudio の特性を調査する
- tab hidden でどうなる?
- 音は停止するそれとも再生し続ける?
調査結果です。
- クロック特性
- performance.now()
- 単位は ms (ms.microsec)
- tab hidden 状態
- 増加する
- OSシャットダウン
- 停止し、リジュームで再開する
- AudioContext.currentTime
- 単位は sec (sec.ms)
- tab hidden 状態
- Chrome, Safari
- 増加する
- iOS
- 停止する
- OSシャットダウン
- 停止し、リジュームで再開する
- サウンド特性
- tab hidden 状態
- Chrome, Safari
- 再生し続ける
- iOS
- 停止する
*/
/*
console.dir({
diffTime1: diffTime1,
"performance.now": now,
"audioContext.currentTime": audioContext.currentTime,
});
*/
// performance.now の分解能は ms 単位
// performance.now() -> 1000 -> 1秒
// 一方 AudioContext.currentTime の分解能は、秒単位
// 44.1kHz = 44100 = 1秒に44100フレームのサンプリングレート
// = 1フレーム辺り 0.00002267573696 秒
// * 4096フレーム = 0.09287981858816秒
// 4096 = 0.09287981859410431
// 44.1kHz で 2048フレーム毎にコールバックすると
// 呼び出し間隔は、 0.04643990929705216
// 計算上は 44100 の 2048 フレームで 0.04643990929408
// iPhone 6s の実測値だと 44100 の 2048 フレームで 0.04643990929705211 (46ms)
// iPhone 5 の実測値だと 44100 の 2048 フレームで 0.04643990929705211 (46ms)
// iPhone 5 の実測値だと 44100 の 256 フレームで 0.005804988662131527 (5ms)
// MBP Chrome 48 の実測値だと 44100 の 2048 フレームで 0.04643990929705211 (46ms)
// 16384 フレームだと 実測値で 0.3715192743764173 (371ms) になる
function processHandler(event) {
var now = performance.now();
var currentTime = audioContext.currentTime;
var playbackTime = event.playbackTime;
nodeA.value = (now / 1000).toFixed(2);
nodeB.value = (currentTime).toFixed(2);
nodeC.value = (playbackTime).toFixed(2);
/*
currentTimeArray.push(currentTime - lastCurrentTime);
timeStampArray.push(playbackTime - lastPlayBackTime);
lastCurrentTime = currentTime;
lastPlayBackTime = playbackTime;
*/
/*
if (currentTimeArray.length > 100) {
scriptProcessor.disconnect();
console.dir(currentTimeArray);
console.dir(timeStampArray);
var now = performance.now();
var diffTime2 = (now / 1000) - currentTime;
console.dir({
diffTime1: diffTime1,
diffTime2: diffTime2,
"performance.now": now,
"audioContext.currentTime": currentTime,
"event.playbackTime": playbackTime,
});
}
*/
var input0 = event.inputBuffer.getChannelData(0); // Float32Array
var output0 = event.outputBuffer.getChannelData(0); // Float32Array
//for(var i = 0; i < bufferSize; ++i) {
// output0[i] = (Math.random() * 2) - 1;
//}
// for (var i = 0; i < bufferSize; ++i) {
// output0[i] = input0[i];
// }
output0.set(input0, 0);
}
function play() {
if (!oscillator) {
oscillator = audioContext.createOscillator();
oscillator.type = "sine";
oscillator.frequency.value = 440;
oscillator.connect(scriptProcessor);
oscillator.start();
}
}
function stop() {
if (oscillator) {
oscillator.stop();
oscillator = null;
}
}
</script>
<!-- take 3 -->
<button onclick="play()">play</button><br />
<button onclick="stop()">stop</button><br />
<input id="a" type="text" value="" /><br />
<input id="b" type="text" value="" /><br />
<input id="c" type="text" value="" /><br />
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
いぷさむ<br>
<script>
var bufferSize = 4096 / 2;
var audioContext = new webkitAudioContext();
var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1);
scriptProcessor.onaudioprocess = processHandler;
scriptProcessor.connect(audioContext.destination);
var oscillator = null;
var currentTimeArray = [];
var timeStampArray = [];
var lastCurrentTime = 0;
var lastPlayBackTime = 0;
var now = performance.now();
/*
var diffTime1 = (now / 1000) - audioContext.currentTime;
*/
var nodeA = document.querySelector("#a");
var nodeB = document.querySelector("#b");
var nodeC = document.querySelector("#c");
function processHandler(event) {
var now = performance.now();
var currentTime = audioContext.currentTime;
var playbackTime = event.playbackTime;
nodeA.value = (now / 1000).toFixed(2);
nodeB.value = (currentTime).toFixed(2);
nodeC.value = (playbackTime).toFixed(2);
/*
currentTimeArray.push(currentTime - lastCurrentTime);
timeStampArray.push(playbackTime - lastPlayBackTime);
lastCurrentTime = currentTime;
lastPlayBackTime = playbackTime;
*/
/*
if (currentTimeArray.length > 100) {
scriptProcessor.disconnect();
console.dir(currentTimeArray);
console.dir(timeStampArray);
var now = performance.now();
var diffTime2 = (now / 1000) - currentTime;
console.dir({
diffTime1: diffTime1,
diffTime2: diffTime2,
"performance.now": now,
"audioContext.currentTime": currentTime,
"event.playbackTime": playbackTime,
});
}
*/
var input0 = event.inputBuffer.getChannelData(0); // Float32Array
var output0 = event.outputBuffer.getChannelData(0); // Float32Array
//for(var i = 0; i < bufferSize; ++i) {
// output0[i] = (Math.random() * 2) - 1;
//}
// for (var i = 0; i < bufferSize; ++i) {
// output0[i] = input0[i];
// }
output0.set(input0, 0);
}
function play() {
if (!oscillator) {
oscillator = audioContext.createOscillator();
oscillator.type = "sine";
oscillator.frequency.value = 440;
oscillator.connect(scriptProcessor);
oscillator.start();
}
}
function stop() {
if (oscillator) {
oscillator.stop();
oscillator = null;
}
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment