Skip to content

Instantly share code, notes, and snippets.

@uupaa
Last active December 7, 2022 08:40
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save uupaa/f3ecf8c50412641b4f43 to your computer and use it in GitHub Desktop.
Save uupaa/f3ecf8c50412641b4f43 to your computer and use it in GitHub Desktop.
Audio, WebAudio, Blob, ArrayBuffer
  1. Audio(url)
  2. Audio(arraybuffer)
  3. Audio(blob)
  4. WebAudio(arraybuffer)
  5. WebAudio(blob)

Audio file access with streaming

http://uupaa.net/issues/3/ の結果

操作方法: loadAudio ▶ 画面が緑になるまで待つ ▶ playAudio の順にクリックする

  • YES - [loadAudio] ▶ 待つ ▶ 緑になる ▶ [playAudio] で再生が始まる

  • 空白 - [loadAudio] ▶ 待つ ▶ 変化がない ▶ [playAudio] で再生も始まらない

  • FUZZY - [loadAudio] ▶ 待つ ▶ 変化がない ▶ [playAudio] で突然再生が始まる
    canplay や progress イベントが信用できない

  • iOS 6.1 - iPhone 3GS iOS 6.1

  • iOS 7.1 - iPhone 4S iOS 7.1

  • iOS sim - iPhone 7.x simulator

  • iOS 8.0 - iPhone 5 iOS 8.0 GM Seed

  • Mac WebKit 6.0.x - Mac Book Pro + WebKit 6.0.x nightly

  • Mac Chrome 39 - Mac Book Pro + Chrome canary 39 (64 bit)

  • Android Chrome 37 - Nexus 7 (2012) Chrome for Android 37

1 2 3 4 5
iOS 6.1 YES FUZZY YES
iOS 7.1 YES YES YES
iOS sim YES YES YES
iOS 8.0 YES YES YES
Mac WebKit 6.0.x YES YES YES
Mac Chrome 39 YES YES YES YES YES
Android Chrome 37 YES FUZZY YES YES

Audio file access without streaming

MBP に簡素なWebサーバを設置(npm install http-server)し http://uupaa.net/issues/3/ と同じテストを行った結果

1 2 3 4 5
iOS 6.1 YES
iOS 7.1 YES YES
iOS sim YES YES
iOS 8.0 YES YES YES YES
Mac WebKit 6.0.x YES YES
Mac Chrome 39 YES YES YES YES YES
Android Chrome 37 YES FUZZY YES YES

雑感

  • <audio>
    • サーバ/Proxyの設定によっては <audio> が機能しないケースがある(よくある)
    • iOS デバイスはバージョンアップで <audio> の実装が密かに変化している
  • WebAudio API は、Blob または ArrayBuffer の両方で利用可能 (iOS 6.x を除く)
  • iPhone 3GS は 256RAM のため、Web Audio は使わないほうがよい(複数ファイルを扱うとメモリ不足で落ちることも)
  • Mac Chrome が優秀なため、全てのシチュエーションで再生できてしまうが、それを鵜呑みにすると、モバイルデバイスで再生されずドハマりする
    • Audio/WebAudio 周りの実装は、必ず実機を用意し、実際の動作を確認しながら行うこと
  • Tegra 2 SoC 搭載な NEON非搭載のAndroid端末では window.AudioContext がなく、Web Audio は利用できない。あきらめてください
<!DOCTYPE html><html><head><title>test</title>
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta charset="utf-8"></head><body>

<p>1. Audio(url)</p>
<input type="button" value="loadAudio" onclick="loadAudio(0)"></input>
<input type="button" value="playAudio" onclick="playAudio(0)"></input>
<input type="button" value="stopAudio" onclick="stopAudio(0)"></input>
<br />
<br />
<p>2. Audio(arraybuffer)</p>
<input type="button" value="loadAudio" onclick="loadAudio(1)"></input>
<input type="button" value="playAudio" onclick="playAudio(1)"></input>
<input type="button" value="stopAudio" onclick="stopAudio(1)"></input>
<br />
<br />
<p>3. Audio(blob)</p>
<input type="button" value="loadAudio" onclick="loadAudio(2)"></input>
<input type="button" value="playAudio" onclick="playAudio(2)"></input>
<input type="button" value="stopAudio" onclick="stopAudio(2)"></input>
<br />
<br />
<p>4. WebAudio(arraybuffer)</p>
<input type="button" value="loadAudio" onclick="loadAudio(3)"></input>
<input type="button" value="playAudio" onclick="playAudio(3)"></input>
<input type="button" value="stopAudio" onclick="stopAudio(3)"></input>
<br />
<br />
<p>5. WebAudio(blob)</p>
<input type="button" value="loadAudio" onclick="loadAudio(4)"></input>
<input type="button" value="playAudio" onclick="playAudio(4)"></input>
<input type="button" value="stopAudio" onclick="stopAudio(4)"></input>
<br />


<script>
var url1 = "./game.m4a";
var stock = {
        0: { node: null, nodeType: "audio",    url: url1, responseType: "",            mime: "", size: 0, data: null, sound: { canplay: false } },
        1: { node: null, nodeType: "audio",    url: url1, responseType: "arraybuffer", mime: "", size: 0, data: null, sound: { canplay: false } },
        2: { node: null, nodeType: "audio",    url: url1, responseType: "blob",        mime: "", size: 0, data: null, sound: { canplay: false } },
        3: { node: null, nodeType: "webaudio", url: url1, responseType: "arraybuffer", mime: "", size: 0, data: null, sound: { buffer: null, source: null } },
        4: { node: null, nodeType: "webaudio", url: url1, responseType: "blob",        mime: "", size: 0, data: null, sound: { buffer: null, source: null } },
    };
var _AudioContext = window.AudioContext ||
                    window.webkitAudioContext;
var webAudioContext = _AudioContext ? new _AudioContext()
                                    : null;

function loadAudio(n) {
    var target = stock[n];

    if (target.responseType) {
        _download(target.url, target.responseType, function(data, mime, size) {
            target.data = data; // blob or arraybuffer
            target.mime = mime;
            target.size = size;
            _downloaded(n);
        });
    } else {
        _downloaded(n);
    }
}

function _download(url, responseType, callback) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
        callback(xhr.response,
                 xhr.getResponseHeader("content-type"),
                 parseInt(xhr.getResponseHeader("content-length")) || 0);
    };
    xhr.responseType = responseType;
    xhr.open("GET", url);
    xhr.send();
}

function _downloaded(n) {
    var target = stock[n];
    var data = target.data;

    if (target.nodeType === "audio") {
        var bloburl = "";
        if (data instanceof ArrayBuffer) {
            bloburl = URL.createObjectURL( new Blob([data], { type: target.mime /* "audio/mp4" */ }) );
        } else if (data instanceof Blob) {
            bloburl = URL.createObjectURL(data);
        } else {
            bloburl = target.url;
        }
        target.node = new Audio();
        target.node.src = bloburl;
        target.node.volume = 0.2;
        target.node.addEventListener("progress", handleEvent, false);
        target.node.addEventListener("canplay", handleEvent, false);
        target.node.load();
    } else {
        if (data instanceof ArrayBuffer) {
            webAudioContext.decodeAudioData(data, function(decodedBuffer) {
                target.sound.buffer = decodedBuffer;
                _ready();
            });
        } else if (data instanceof Blob) {
            var reader = new FileReader();
            reader.onloadend = function() {
                webAudioContext.decodeAudioData(reader.result, function(decodedBuffer) {
                    target.sound.buffer = decodedBuffer;
                    _ready();
                });
            };
            reader.readAsArrayBuffer(data);
        }
    }

    function handleEvent(event) {
        var duration = target.node.duration;

        switch (event.type) {
        case "canplay":
            target.node.removeEventListener("canplay", handleEvent, false);
            target.sound.canplay = true;
            break;
        case "progress":
            break;
        }
        if (duration > 0 && target.sound.canplay) {
            target.node.removeEventListener("progress", handleEvent, false);
            _ready();
        }
    }
}

function playAudio(n) {
    var target = stock[n];

    if (target.nodeType === "audio") {
        target.node.play();
    } else if (target.nodeType === "webaudio") {
        stopAudio(n);
        target.sound.source = webAudioContext.createBufferSource();
        target.sound.source.buffer = target.sound.buffer;
        target.sound.source.connect(webAudioContext.destination);
        if (target.sound.source.start) {
            target.sound.source.start(0);
        } else {
            target.sound.source.noteOn(0); // [iOS 6.x]
        }
    }
}

function stopAudio(n) {
    var target = stock[n];

    if (target.nodeType === "audio") {
        if (target.node) {
            target.node.pause();
        }
    } else if (target.nodeType === "webaudio") {
        if (target.sound.source) {
            if (target.sound.source.stop) {
                target.sound.source.stop(0);
            } else {
                target.sound.source.noteOff(0); // [iOS 6.x]
            }
            target.sound.source = null;
        }
    }
}

var lime = 80;
function _ready() {
    lime += 32;
    document.body.style.cssText = "background-color: rgb(0, " + lime + ", 0)";
}

</script>
</body></html>

iOS デバイスにおける WebAudio の制限

window.onload のタイミングではサウンドファイルのロードと decodeAudioData によるデコードまで実行可能

var ctx = new AudioContext();
var decodedBuffer = null;
    
var xhr = new XMLHttpRequest();

xhr.responseType = "arraybuffer";
xhr.onload = function() {
    ctx.decodeAudioData(xhr.response, function(buffer) {
        decodedBuffer = buffer;
    });
};
xhr.open("GET", url);
xhr.send();

一度ユーザアクション由来のイベントハンドラ内で source.start を実行しないと、音が鳴らない

document.body.onclick = function() {
    var source = ctx.createBufferSource();

    source.buffer = decodedBuffer;
    source.connect(ctx.destination);
    source.start(0);
};

Auto fade

WebAudio で再生中に、タブの切替やサスペンドでボリューム操作が自動的に行われるかどうかについて調査

タブ切り替え サスペンド
iPhone 5 (iOS 8) YES YES
iPhone 4S (iOS 7) YES
Nexus 7 (2012)(Android 4.4.4)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment