Skip to content

WebAssembly ガイド

libsonare は WebAssembly にコンパイルでき、ブラウザで直接オーディオ解析が可能です。

インストール

パッケージ未公開

npm パッケージ @libraz/sonare は現在ベータ版で、まだ公開されていません。現時点では、リポジトリから直接 WASM ファイルを使用するか、ソースからビルドしてください。

npm/yarn

bash
# 未公開 - 近日公開予定
npm install @libraz/sonare
# または
yarn add @libraz/sonare

CDN

html
<script type="module">
  import { init, detectBpm } from 'https://unpkg.com/@libraz/sonare';
</script>

基本的な使い方

typescript
import { init, detectBpm, detectKey, analyze } from '@libraz/sonare';

async function analyzeAudio() {
  // WASM モジュールを初期化
  await init();

  // AudioContext からオーディオデータを取得
  const audioCtx = new AudioContext();
  const response = await fetch('music.mp3');
  const arrayBuffer = await response.arrayBuffer();
  const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);

  // モノラルサンプルを取得
  const samples = audioBuffer.getChannelData(0);
  const sampleRate = audioBuffer.sampleRate;

  // BPM を検出
  const bpm = detectBpm(samples, sampleRate);
  console.log(`BPM: ${bpm}`);

  // キーを検出
  const key = detectKey(samples, sampleRate);
  console.log(`キー: ${key.name}`);

  // 完全解析
  const result = analyze(samples, sampleRate);
  console.log(result);
}

ファイル入力

typescript
async function analyzeFile(file: File) {
  await init();
  const audioCtx = new AudioContext();

  const arrayBuffer = await file.arrayBuffer();
  const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
  const samples = audioBuffer.getChannelData(0);

  return analyze(samples, audioBuffer.sampleRate);
}

// ファイル入力での使用
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const result = await analyzeFile(file);
  console.log(`BPM: ${result.bpm}`);
});

進捗レポート

typescript
import { init, analyzeWithProgress } from '@libraz/sonare';

await init();

const result = analyzeWithProgress(samples, sampleRate, (progress, stage) => {
  const percent = Math.round(progress * 100);
  console.log(`${stage}: ${percent}%`);

  // UI を更新
  progressBar.style.width = `${percent}%`;
  statusText.textContent = stage;
});

Web Worker の使用

メインスレッドをブロックしないように解析を Web Worker にオフロードします。

worker.ts:

typescript
import { init, analyze, AnalysisResult } from '@libraz/sonare';

let initialized = false;

self.onmessage = async (e: MessageEvent) => {
  const { samples, sampleRate } = e.data;

  if (!initialized) {
    await init();
    initialized = true;
  }

  try {
    const result = analyze(samples, sampleRate);
    self.postMessage({ success: true, result });
  } catch (error) {
    self.postMessage({ success: false, error: error.message });
  }
};

main.ts:

typescript
const worker = new Worker(new URL('./worker.ts', import.meta.url), {
  type: 'module'
});

function analyzeInWorker(
  samples: Float32Array,
  sampleRate: number
): Promise<AnalysisResult> {
  return new Promise((resolve, reject) => {
    worker.onmessage = (e) => {
      if (e.data.success) {
        resolve(e.data.result);
      } else {
        reject(new Error(e.data.error));
      }
    };
    worker.postMessage({ samples, sampleRate });
  });
}

ステレオからモノラルへの変換

typescript
async function getMonoSamples(audioBuffer: AudioBuffer): Promise<Float32Array> {
  if (audioBuffer.numberOfChannels === 1) {
    return audioBuffer.getChannelData(0);
  }

  // ステレオをモノラルにミックス
  const left = audioBuffer.getChannelData(0);
  const right = audioBuffer.getChannelData(1);
  const mono = new Float32Array(left.length);

  for (let i = 0; i < left.length; i++) {
    mono[i] = (left[i] + right[i]) / 2;
  }

  return mono;
}

パフォーマンスのヒント

ダウンサンプリング

BPM 検出には 22050 Hz で十分です:

typescript
import { resample, detectBpm } from '@libraz/sonare';

// 高速解析のためにダウンサンプル
const downsampled = resample(samples, 48000, 22050);
const bpm = detectBpm(downsampled, 22050);

セグメント解析

長いファイルの場合、関連するセクションのみを解析:

typescript
function analyzeSegment(
  samples: Float32Array,
  sampleRate: number,
  startSec: number,
  endSec: number
) {
  const start = Math.floor(startSec * sampleRate);
  const end = Math.floor(endSec * sampleRate);
  const segment = samples.slice(start, end);

  return analyze(segment, sampleRate);
}

// サビのみを解析(60-90秒)
const result = analyzeSegment(samples, sampleRate, 60, 90);

React の例

tsx
import { useState } from 'react';
import { init, analyzeWithProgress, AnalysisResult } from '@libraz/sonare';

function AudioAnalyzer() {
  const [progress, setProgress] = useState(0);
  const [stage, setStage] = useState('');
  const [result, setResult] = useState<AnalysisResult | null>(null);

  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    await init();

    const audioCtx = new AudioContext();
    const arrayBuffer = await file.arrayBuffer();
    const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
    const samples = audioBuffer.getChannelData(0);

    const analysisResult = analyzeWithProgress(
      samples,
      audioBuffer.sampleRate,
      (p, s) => {
        setProgress(p);
        setStage(s);
      }
    );

    setResult(analysisResult);
  };

  return (
    <div>
      <input type="file" accept="audio/*" onChange={handleFileChange} />

      {stage && (
        <div>
          <div>{stage}: {Math.round(progress * 100)}%</div>
          <progress value={progress} max={1} />
        </div>
      )}

      {result && (
        <div>
          <p>BPM: {result.bpm.toFixed(1)}</p>
          <p>キー: {result.key.name}</p>
        </div>
      )}
    </div>
  );
}

ストリーミング解析

ストリーミング API は低レイテンシでリアルタイム音声解析を実現します。バッチ解析とは異なり、音声が到着するたびにチャンクごとに処理します。

バッチ vs ストリーミング

アプローチ用途レイテンシ機能
バッチ録音済みファイル完全解析(BPM、コード、セクション)
ストリーミングライブ音声、ビジュアライゼーション低(〜10ms)Mel、クロマ、オンセット、プログレッシブ BPM/キー

アーキテクチャ概要

基本的な例

typescript
import { init, StreamAnalyzer } from '@libraz/sonare';

async function setupStreaming() {
  await init();

  const audioCtx = new AudioContext();
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const source = audioCtx.createMediaStreamSource(stream);

  // 60fps 用にスロットリングしたアナライザーを作成
  const analyzer = new StreamAnalyzer(
    audioCtx.sampleRate,
    2048,   // nFft
    512,    // hopLength
    128,    // nMels
    true,   // computeMel
    true,   // computeChroma
    true,   // computeOnset
    4       // 4 フレームごとに出力(44100Hz で約 60fps)
  );

  // シンプルさのため ScriptProcessor を使用(本番では AudioWorklet 推奨)
  const processor = audioCtx.createScriptProcessor(512, 1, 1);

  processor.onaudioprocess = (e) => {
    const input = e.inputBuffer.getChannelData(0);
    analyzer.process(input);

    const available = analyzer.availableFrames();
    if (available > 0) {
      const frames = analyzer.readFramesSoa(available);
      updateVisualization(frames);

      // プログレッシブ BPM/キー推定をチェック
      const stats = analyzer.stats();
      if (stats.estimate.updated) {
        console.log(`BPM: ${stats.estimate.bpm.toFixed(1)}`);
        console.log(`キー: ${stats.estimate.key}`);
      }
    }
  };

  source.connect(processor);
  processor.connect(audioCtx.destination);
}

帯域幅最適化

ニーズに応じて適切な出力形式を選択:

形式フレームあたりサイズ用途
readFramesSoa()〜600 バイト開発、デバッグ
readFramesI16()〜300 バイト高品質ビジュアライゼーション
readFramesU8()〜150 バイトモバイル、帯域幅制限環境

プログレッシブ推定

ストリーミング API は時間とともに精度が向上するプログレッシブ BPM/キー推定を提供:

typescript
const stats = analyzer.stats();

// BPM(約 5 秒後に利用可能)
if (stats.estimate.bpm > 0) {
  const confidence = stats.estimate.bpmConfidence;
  console.log(`BPM: ${stats.estimate.bpm.toFixed(1)} (${(confidence * 100).toFixed(0)}%)`);
}

// キー(約 10 秒後に利用可能)
if (stats.estimate.key >= 0) {
  const keyNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
  const keyName = keyNames[stats.estimate.key];
  const mode = stats.estimate.keyMinor ? 'マイナー' : 'メジャー';
  console.log(`キー: ${keyName} ${mode}`);
}

ブラウザ互換性

ブラウザ最小バージョン
Chrome57+
Firefox52+
Safari11+
Edge16+

要件:

  • WebAssembly サポート
  • Web Audio API
  • ES2017+ (async/await)

バンドルサイズ

ファイルサイズGzip
sonare.js~34 KB~12 KB
sonare.wasm~228 KB~80 KB
合計~262 KB~92 KB

トラブルシューティング

AudioContext が許可されない

モダンブラウザは AudioContext を作成する前にユーザー操作が必要:

typescript
document.addEventListener('click', async () => {
  const audioCtx = new AudioContext();
  await audioCtx.resume();
});

クロスオリジンの問題

他のドメインからオーディオを読み込む場合:

typescript
const response = await fetch(url, {
  mode: 'cors',
  credentials: 'omit'
});

メモリの問題

非常に長いオーディオファイルの場合、チャンクで解析を検討:

typescript
const CHUNK_DURATION = 60; // 秒

for (let start = 0; start < totalDuration; start += CHUNK_DURATION) {
  const chunk = samples.slice(
    start * sampleRate,
    (start + CHUNK_DURATION) * sampleRate
  );
  // チャンクを解析
}

Released under the Apache-2.0 License.