Web Audio APIだけで音声編集アプリを作る:ブラウザ内処理の仕組みを実装レベルで解説

音声ファイルをサーバにアップロードせず、ブラウザだけでカット・変換・音量調整ができるWebアプリがあります。これを支えているのが Web Audio API です。今回は「ブラウザ内で音声編集が完結する仕組み」を、実装レベルのコードとともに解説します。

1. decodeAudioData:ファイルを AudioBuffer(32bit float PCM)に変換する

Web Audio API の入口は AudioContext と decodeAudioData です。MP3 や WAV といったファイルを、編集可能な PCM データ(AudioBuffer)にデコードします。

const ctx = new AudioContext(); const res = await fetch('voice.mp3'); const arrayBuffer = await res.arrayBuffer(); const audioBuffer = await ctx.decodeAudioData(arrayBuffer); // audioBuffer.sampleRate → 44100 など // audioBuffer.numberOfChannels → 1(モノラル)or 2(ステレオ) // audioBuffer.getChannelData(0) → Float32Array(32bit float PCM、-1.0〜1.0) 

AudioBuffer の中身は、チャンネルごとの Float32Array です。「編集する」とは、この配列を直接いじることに他なりません。入力フォーマットの差(MP3 / WAV / M4A など)は decodeAudioData が吸収してくれますが、対応コーデックはブラウザ依存です(例:Safari は OGG/Vorbis をデコードできません)。

2. 波形選択とトリミング:秒数をサンプル数に直すだけ

波形上で範囲を選んで切り出す処理は、考え方はシンプルです。秒数をサンプル数に変換し、配列を切り出すだけです。

function trim(buffer, startSec, endSec, ctx) { const sr = buffer.sampleRate; const start = Math.floor(startSec * sr); const length = Math.floor((endSec - startSec) * sr); const out = ctx.createBuffer(buffer.numberOfChannels, length, sr); for (let ch = 0; ch < buffer.numberOfChannels; ch++) { out.getChannelData(ch).set( buffer.getChannelData(ch).subarray(start, start + length) ); } return out; } 

ミリ秒単位の精密な選択も同じ理屈です。波形の描画自体は getChannelData の値を間引いて canvas にプロットすれば実現できます。

3. 削除時の 4ms クロスフェード:つなぎ目の「プチッ」を消す

区間を削除して前後をつなぐと、波形の段差で「プチッ」というクリックノイズが発生します。これを防ぐ定番の手法が、つなぎ目にごく短いクロスフェードをかけることです。

function spliceWithCrossfade(head, tail, sampleRate) { const fadeLen = Math.floor(sampleRate * 0.004); // 4ms // head の末尾 fadeLen サンプルを、tail の先頭とミックスする for (let ch = 0; ch < head.numberOfChannels; ch++) { const h = head.getChannelData(ch); const t = tail.getChannelData(ch); for (let i = 0; i < fadeLen; i++) { const k = i / fadeLen; // 0 → 1 const idx = h.length - fadeLen + i; h[idx] = h[idx] * (1 - k) + t[i] * k; } } // あとは head と tail.slice(fadeLen) を連結する } 

4ms という短さなら音の違和感はほぼなく、段差だけがきれいに消えます。

4. ピーク正規化:ピーク検出 → ゲイン計算

「全体の音量を、最大ピークが -1dBFS になるように揃える」処理です。手順は3ステップです。

  1. 全チャンネル・全サンプルを走査し、絶対値の最大(ピーク)を求める
  2. 目標値 ÷ ピーク でゲインを計算する(-1dBFS ≒ 0.891)
  3. 全サンプルにゲインを掛ける
function peakNormalize(buffer, targetDb = -1) { let peak = 0; for (let ch = 0; ch < buffer.numberOfChannels; ch++) { const d = buffer.getChannelData(ch); for (let i = 0; i < d.length; i++) { const v = Math.abs(d[i]); if (v > peak) peak = v; } } const gain = Math.pow(10, targetDb / 20) / peak; for (let ch = 0; ch < buffer.numberOfChannels; ch++) { const d = buffer.getChannelData(ch); for (let i = 0; i < d.length; i++) d[i] *= gain; } } 

注意点として、すでに音割れしている(クリッピング済みの)素材はこの処理では直りません。正規化は「小さすぎる音を安全に持ち上げる」ための処理です。

5. エンコード:WAV 16bit 出力と lamejs による MP3 化

編集後の AudioBuffer は float32 のままではファイルに保存できません。WAV なら 16bit PCM に変換してヘッダを付けるだけです。

// Float32(-1.0〜1.0)→ Int16 への変換 function floatTo16BitPCM(view, offset, input) { for (let i = 0; i < input.length; i++, offset += 2) { const s = Math.max(-1, Math.min(1, input[i])); view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); } } 

MP3 はブラウザ標準 API ではエンコードできないため、LAME の JavaScript 移植版である lamejs を使います。ビットレートは 96〜320kbps の範囲で指定できます。

import { Mp3Encoder } from 'lamejs'; const encoder = new Mp3Encoder(2, 44100, 128); // ch, sampleRate, kbps // Int16 化したサンプルをブロックごとに入力 → encoder.encodeBuffer() // 最後に encoder.flush() で残りのデータを吐き出す 

実測ではデスクトップ環境で約5倍速(1分の音声を約12秒でエンコード)程度の性能が出ます。すべてブラウザ内(メインスレッド〜Worker)で完結し、サーバ通信はゼロです。

6. ブラウザ差とメモリの現実

実装上ぶつかる壁は主に2つです。

  • コーデック差:decodeAudioData の対応形式はブラウザ依存です。MP3 / WAV はほぼ共通ですが、OGG は Safari 非対応、Opus の扱いにも差があります。対応入力形式の表は「実際にデコードできるか」の実測で作るのが確実です。
  • メモリ:PCM は非圧縮なので重いのが現実です。44.1kHz ステレオで1時間分 = 44100 × 2ch × 4byte × 3600秒 ≒ 1.3GB。実用的な設計としては、400MB(約19分)を超えたあたりで警告を出すのが無難です。

7. プライバシー設計:ローカル処理 = アップロードなし

この方式の最大の利点は、音声データがユーザーの PC から一歩も出ないことです。サーバに送らないので通信の待ち時間がなく、サーバ側での保存・漏洩リスクも存在しません。「無料・登録不要」がビジネスとして成立するのも、サーバコストがほぼゼロだからです。

この仕組みをそのまま使った無料ツール例として、17NAS Online Audio Tools があります。登録不要・透かしなしで、ここで解説したトリミングやピーク正規化、MP3/WAV 変換などがブラウザ内だけで動きます。