Web Audio API
で作った音を保存したい。
前回作ったピアノ音をブラウザで wav
形式に保存することを考えます。
Web Audio API
の記事は思ったより少なくマイクなどからの録音関係の内容ばかりだったので、手探りで進めていきます。
マイク録音をする記事ですが「ブラウザで録音してwavで保存」のコードをメインで参考にしています。
ScriptProcessorNode
とその後継の AudioWorklet
音が鳴るたびに処理を挟むような用途で使う。
参考元でも言及される AudioWorklet
ですが数年前から最近の記事まで「実験的、限定的、特定の環境でのみ動く」など書かれていたのでイマイチ手を出しにくい。
一応使ってみましたが AudioWorkletProcessor
が見つからなかった。
全体的な処理の流れ
参考元ではマイク音声を navigator.mediaDevices.getUserMedia
からとってきて MediaStream
を作って処理ノードを通して出力しているようです。
このストリームの代わりに前回作った GainNode
をコネクトすればよさそうです。
ScriptProcessorNode
から AudioDestinationNode
に繋いでもいいはずですが、今回のコードだと音が出なくなったのでこの形で作ります。この方法で動かなくなったので追記参照
実際のコード
GainNode
をScript
に繋いでonAudioProcess
で配列に入れる。
配列をexportWAV
でblob
形式にして保存させます。
exportWAV
は TypeScript で書いて変数をできるだけ const
に変えてますが中身は参考元と同じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
const bufferSize = 1024; const audioData: any = []; const onAudioProcess = (e: AudioProcessingEvent) => { var input = e.inputBuffer.getChannelData(0); var bufferData = new Float32Array(bufferSize); for (var i = 0; i < bufferSize; i++) { bufferData[i] = input[i]; } audioData.push(bufferData); }; export const saveBuff = (ctx: AudioContext, gain: GainNode) => { const scriptProcessor = ctx.createScriptProcessor(bufferSize, 1, 1); gain.connect(scriptProcessor); scriptProcessor.onaudioprocess = onAudioProcess; //scriptProcessor.connect(ctx.destination); setTimeout(() => { const downloadLink = document.createElement("a"); downloadLink.href = exportWAV(audioData, ctx.sampleRate); downloadLink.download = "test.wav"; downloadLink.click(); }, 10000); }; /** Buffer -> URL */ const exportWAV = (audioData: any, audio_sample_rate: number) => { const encodeWAV = (samples: Float32Array, sampleRate: number) => { const buffer = new ArrayBuffer(44 + samples.length * 2); const view = new DataView(buffer); const writeString = (view: any, offset: any, str: string) => { for (let i = 0; i < str.length; i++) { view.setUint8(offset + i, str.charCodeAt(i)); } }; const floatTo16BitPCM = (output: any, offset: any, input: any) => { for (let i = 0; i < input.length; i++, offset += 2) { const s = Math.max(-1, Math.min(1, input[i])); output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true); } }; writeString(view, 0, "RIFF"); // RIFFヘッダ view.setUint32(4, 32 + samples.length * 2, true); // これ以降のファイルサイズ writeString(view, 8, "WAVE"); // WAVEヘッダ writeString(view, 12, "fmt "); // fmtチャンク view.setUint32(16, 16, true); // fmtチャンクのバイト数 view.setUint16(20, 1, true); // フォーマットID view.setUint16(22, 1, true); // チャンネル数 view.setUint32(24, sampleRate, true); // サンプリングレート view.setUint32(28, sampleRate * 2, true); // データ速度 view.setUint16(32, 2, true); // ブロックサイズ view.setUint16(34, 16, true); // サンプルあたりのビット数 writeString(view, 36, "data"); // dataチャンク view.setUint32(40, samples.length * 2, true); // 波形データのバイト数 floatTo16BitPCM(view, 44, samples); // 波形データ return view; }; const mergeBuffers = (audioData: any) => { const sl = audioData.reduce((a: number, c: any) => a + c.length, 0); const samples = new Float32Array(sl); let sampleIdx = 0; for (let i = 0; i < audioData.length; i++) { for (let j = 0; j < audioData[i].length; j++) { samples[sampleIdx] = audioData[i][j]; sampleIdx++; } } return samples; }; const dataview = encodeWAV(mergeBuffers(audioData), audio_sample_rate); const audioBlob = new Blob([dataview], { type: "audio/wav" }); const myURL = window.URL || window.webkitURL; const url = myURL.createObjectURL(audioBlob); return url; }; |
前回のコードに1行追記して使えます。
require("./save").saveBuff(audioContext, gain);
10秒で切ってるので終わりがぶつ切りですが、ちゃんと保存できてます。
あとはちゃんと終了検知して、リセットもできるようにしておけばいいかな。
もっと簡単な方法とかもあるんじゃないかと思いつつも、やりたいことが出来たのでとりあえず満足です。
追記
コード整形しているうちに上記コードで保存できなくなった。
どうも onaudioprocess
が動作していない様子。
destination
につなげると動くようになるので正しくつなげる必要があるってことなんでしょうが、じゃあなんで前は動いたんだ。
多少の不信感を覚えつつも正しく ScriptProcessorNode
をつなぎます。
上記コードのままだと output
が空で、保存はできるが音は出ない状態なので入力をそのまま出力に渡す処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 |
const onAudioProcess = (e: AudioProcessingEvent) => { const input = e.inputBuffer.getChannelData(0); const output = e.outputBuffer.getChannelData(0); for (let i = 0; i < input.length; i++) output[i] = input[i]; const bufferData = new Float32Array(bufferSize); for (let i = 0; i < bufferSize; i++) { bufferData[i] = input[i]; } audioData.push(bufferData); }; |
正しい理屈でちゃんと動いたのはいいんですが、じゃあなんで最初はこうならなかったんだという気持ちも。多分初心者特有の勘違いで何かやらかしてたんだろうと思うことにします。