MediaPipe音声分類入門:YAMNetで音のラベルを読む
MediaPipeは画像だけのライブラリではありません。AudioClassifierへ音の波形を渡すと、モデルが学習した音の種類とスコアを受け取れます。ここでは録音やマイクを使わず、JavaScriptで作った無音・正弦波・ノイズを実際に分類して、音声入力の形と結果の読み方を学びます。
1. この記事のゴールと使用する版
音声をFloat32Arrayとサンプルレートで表し、AudioClassifierで分類できること。分類と文字起こしを区別し、音声ファイルのデコード・モノラル化・区間ごとの結果を理解することを目指します。
前提はHTMLの要素、JavaScriptの変数・関数・イベント、async/awaitの基本です。モデルを自分で学習させる知識は不要です。CDN版のSDKを使用するため、このサンプルへMediaPipeをnpmで追加インストールする必要はありません。SDKとWASMは同じパッケージ・同じ版に固定します。
教材の構成:@mediapipe/tasks-audio 1.1.0。モデルはYAMNet、float32、モデル版1。2026年10月12日にnpm公式レジストリでtasks-audioの最新版を確認しました。visionの0.10.34とは別パッケージ・別の版です。
このサイトのAI画像ツールで使うSDKと配信方法、旧Solutionsとの違いは概要記事の現在のサイト構成に整理しています。音声・テキスト編は追加の学習教材です。今回のモデルは公式ストレージの版が分かるURLを指定し、コードの先頭で確認できます。
2. サンプルを試し、自分のPCで起動する
VS Codeでフォルダーを開く方法や、app.jsの編集・保存が初めてなら、導入記事のエディター準備と「保存 → ブラウザ更新 → 実行」の手順を先に確認してください。下では、このサンプルの起動に必要な操作を示します。
合成サンプルの「無音」を選び、「選んだ音を分類する」を押します。波形と上位ラベル、JSONを確認します。次に440 Hzの正弦波とノイズへ切り替えて再実行します。これは本物のAudioClassifierへの入力ですが、録音した声や猫の鳴き声の精度を示す作例ではありません。音は自動再生されません。

無音と正弦波を実際に比べる
無音では3区間の結果が返り、最初の区間の上位候補はSilence、スコア0.80078でした。同じ環境で440 Hzの合成正弦波へ切り替えると、最初の区間はSine waveが0.96875になりました。表は最初の区間だけなので、JSONの後続区間も確認します。

正弦波の最初の区間ではTelephoneも0.14844でした。これらの音カテゴリは互いに排他的な一択の割合ではなく、表示スコアの合計は1を超えます。合計100%の円グラフへ置き換えたり、「音の96.875%が正弦波」と読んだりせず、各ラベルに対するモデルのスコアとして扱います。
ZIPを展開して、HTMLを直接開かずHTTPで起動する
- ZIPを保存し、エクスプローラーの「すべて展開」で中身を展開します。ZIPの中を閲覧している状態で実行しません。
index.htmlとapp.jsが見えるフォルダーで右クリックし、ターミナルを開きます。README.txtにも手順があります。- 下の最初の2行を1行ずつ実行します。バージョンが表示されればNode.jsとnpmを利用できます。「見つからない」ならNode.js公式サイトのLTS版を導入し、ターミナルを開き直します。
- 3行目を実行します。
http-serverの初回取得には通信が必要です。サーバーが起動したらターミナルは開いたままにします。 - ブラウザのアドレス欄へ
http://localhost:8080/を入力します。サンプルが表示されたら上の操作を試します。 - 編集後は保存してブラウザを再読み込みします。終了はターミナルでCtrl+Cです。
node --version
npm.cmd --version
npx.cmd --yes http-server . -p 8080 Windows PowerShellではnpm.cmdとnpx.cmdを使うと、スクリプト実行ポリシーによるつまずきを避けやすくなります。macOS・Linuxはnpm・npxで実行します。8080が使用中なら-p 8081へ変え、ブラウザ側もhttp://localhost:8081/に合わせます。
初回のボタン操作でSDK・WASM・モデルを取得するので、しばらく待つ場合があります。HTTPサーバーはHTMLとJavaScriptを配信するためのもので、AI推論をサーバー側で行う構成ではありません。file://での直接起動は使いません。基本操作の確認はPCのブラウザで行い、スマホは画面幅の確認と実機での動作検証を区別します。
3. 音声分類は文字起こしとは違う
このタスクは、音に対して学習済みのカテゴリ候補を返します。人が話した言葉を文章へ変換する文字起こしや、音源を分離して歌声だけ取り出す処理とは異なります。YAMNetの候補には声・音楽・環境音などのラベルがありますが、そのラベル一覧はモデルが持っています。HTMLに新しいラベル名を書き足すだけでは、新しい音を学習したことにはなりません。
合成の正弦波は、周波数が一定の数学的な音です。ノイズも決定的な擬似乱数で作ります。毎回同じ入力を作りやすく、ファイル形式や録音条件の問題を切り分ける教材として使っています。一方で、自然な音の正しいカテゴリを評価するデータではありません。予想と違うラベルが出ても、まずJSONが返るまでの導入確認と、モデルの適用範囲を分けて考えます。
このサイトのAI画像ツールはVision系の入力を使います。音声編はMediaPipe全体を理解する追加教材で、現在の画像ツールへ音声分類が搭載されているという意味ではありません。SDK、WASM、モデル、入力、結果表示を分ける考え方は画像編と共通です。
4. 波形の配列とHzをセットで渡す
音の波形は、時間に沿った振幅の数値です。サンプルではFloat32Arrayを使い、16,000 Hzで2秒なら32,000個の値を用意します。Hzは1秒あたりのサンプル数で、音の長さとは別です。ゼロで埋まった配列は無音になります。
const sampleRate = 16000;
const samples = new Float32Array(sampleRate * 2); // 2秒の無音
const results = classifier.classify(samples, sampleRate);正弦波では各位置へsin()の値を入れ、振幅は0.15に抑えています。波形は処理内容を確認する表示で、今回は音の再生は行いません。配列の長さをサンプルレートで割ると、秒数を求められます。
classify()の第2引数を省略すると、SDKの既定サンプルレートが使われます。実際の配列が16,000 Hzなのに別の値として渡すと、音の時間・周波数の解釈が変わってしまいます。ファイルを読み込んだ場合も、固定の16,000を無条件に渡さず、デコードしたAudioBuffer.sampleRateと揃えます。必要なリサンプリング等はタスク側の処理へ渡します。
5. 音声ファイルを読み込む場合
自分の音声ファイルを選ぶと、arrayBuffer()でバイト列を取り、Web Audio APIのdecodeAudioData()で波形へ変換します。ブラウザが対応する形式である必要があり、拡張子を変えるだけでは音声形式は変換できません。
const audioContext = new AudioContext();
const buffer = await audioContext.decodeAudioData(await file.arrayBuffer());
const samples = buffer.getChannelData(0);
const results = classifier.classify(samples, buffer.sampleRate);
await audioContext.close();上の短い例は最初のチャンネルを使います。完全コードは複数チャンネルを平均してモノラルの配列にします。これは左右の音を混ぜる教材向けの単純な方法で、左右が逆位相なら打ち消される場合もあります。音の扱いを厳密にする用途では、どのチャンネルを使うかを要件に合わせて決めます。
教材では入力を20 MB以下、処理する長さを冒頭15秒までに制限しています。ただし、decodeAudioData()はファイル全体をデコードしてから一部を使うため、長い音声のメモリ負荷まで完全に抑える仕組みではありません。大きなファイルを扱う本番設計では、長さ・形式・メモリを追加で検討します。作ったAudioContextは、デコード後にclose()します。
6. 時間区間とスコアを読む
音声分類の結果は配列です。一つの音声クリップに対して、時間区間ごとの分類が複数返る場合があります。表は最初の区間の候補だけを表示し、JSONには全区間を出しています。表のラベルを、音声全体の代表ラベルだと決めつけないようにしましょう。
const firstWindow = results[0];
const categories = firstWindow.classifications[0].categories;
for (const category of categories) {
console.log(category.categoryName, category.score);
}categoryNameが音の候補、scoreがモデルのスコアです。上位5件を表示します。似た音の候補が複数出ることがあり、スコアを「その音が確実に存在する確率」として利用者へ断定するのは避けます。入力素材を増やし、対象の音・背景雑音・録音機器を揃えて検証してから、閾値や表示文を決めます。
文字列のラベルは英語の場合があります。表示ラベルの言語を変える設定は、モデルに該当の表示名がある場合に働くもので、音の認識範囲を増やす設定ではありません。日本語の会話内容を理解したいなら、音イベント分類だけで要件を満たすかを先に確認します。
classify()は同期的に動き、長い処理はUIを止めます。今回のボタン1回の教材で仕組みを学んだ後、連続音声に広げるなら実行頻度やWeb Workerの利用を検討します。このサンプルはマイク権限を要求せず、利用者が選んだファイルを外部へ送信する処理も含みません。SDKとモデルの取得には外部への通信があります。
7. 完全コードを確認する
以下はダウンロードZIPと同じソースです。index.htmlが画面と操作ボタン、app.jsがSDKの初期化・入力の準備・推論・出力表示を担当します。コードを途中だけコピーすると必要な要素が欠けるので、最初はZIPの完全版から起動してください。慣れたら設定を1つずつ変更し、JSONと画面の変化を見ます。
index.html:画面の完全コード
<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><title>MediaPipeで音の種類を分類する | 猫の手ツール</title><style>:root{font-family:system-ui,sans-serif;color:#17243c;background:#eef2f8}*{box-sizing:border-box}body{margin:0}main{max-width:1080px;margin:auto;padding:28px 20px}h1{font-size:clamp(1.7rem,5vw,2.4rem)}p{line-height:1.8}button,input,select,textarea{font:inherit}button{background:#4338ca;color:white;border:0;border-radius:8px;padding:12px 18px;cursor:pointer}button:disabled{opacity:.55;cursor:wait}label{display:block;font-weight:650;margin:12px 0 6px}textarea{width:100%;min-height:100px;padding:12px;border:1px solid #aab7ce;border-radius:8px}select,input[type=file]{max-width:100%;padding:8px}section,.box{background:white;border:1px solid #dbe1eb;border-radius:14px;padding:18px;margin:18px 0}.controls{display:flex;flex-wrap:wrap;gap:12px;align-items:center}#status{border-left:4px solid #4338ca;padding:14px;background:white;min-height:54px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}canvas,video{max-width:100%;height:auto;display:block;border-radius:8px;background:#14243b}table{border-collapse:collapse;width:100%;font-size:.9rem}th,td{text-align:left;border-bottom:1px solid #dbe1eb;padding:10px}pre{background:#12203a;color:#e0e8ff;padding:16px;border-radius:8px;white-space:pre-wrap;overflow-wrap:anywhere;max-height:420px;overflow:auto}.scroll{overflow:auto}meter{width:160px;max-width:100%}a{color:#4338ca}small{color:#53637d}button:focus-visible,a:focus-visible{outline:3px solid #e99a23;outline-offset:3px}@media(max-width:640px){.grid{grid-template-columns:1fr}main{padding:18px 12px}section{padding:14px}}</style></head><body><main><p><a href="./README.txt">起動手順を開く</a></p><h1>MediaPipeで音の種類を分類する</h1><p>合成した無音・440 Hzの正弦波・ノイズを本物のAudioClassifierへ入力します。録音した音声ではありません。音は自動再生しません。</p><section><h2>実行する</h2><label for="sample">合成サンプル(2秒・16,000 Hz)</label><select id="sample"><option value="silence">無音</option><option value="sine">440 Hzの正弦波</option><option value="noise">決定的な擬似乱数ノイズ</option></select><label for="audio-file">または音声ファイル(20 MB以下、冒頭15秒を使用)</label><input id="audio-file" type="file" accept="audio/*"><div class="controls"><button id="run-button">選んだ音を分類する</button></div></section><p id="status" role="status" aria-live="polite">準備できました。ボタンを押すとSDK・モデルを読み込みます。</p><section><h2>入力の波形</h2><canvas id="input-canvas" width="960" height="180"></canvas><p id="input-info">未実行</p></section><section><h2>最初の区間の分類結果</h2><table><thead><tr><th>ラベル</th><th>スコア</th><th>目安</th></tr></thead><tbody id="results-body"></tbody></table><p><small>自然音・録音データの精度を証明するサンプルではありません。分類ラベルはモデルの候補です。</small></p></section><section><h2>実際の出力JSON・検証条件</h2><pre id="json-output">まだ実行していません。</pre></section><p><small>推論はブラウザ内で行います。SDK・モデル取得のためCDN / Googleの公開ストレージへ接続します。入力データを送信する処理はありません。記事の検証日:2026年10月12日。</small></p></main><script type="module" src="./app.js"></script></body></html>app.js:処理の完全コード
const VERSION = '1.1.0';
const BASE = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-audio@${VERSION}`;
const MODEL = 'https://storage.googleapis.com/mediapipe-models/audio_classifier/yamnet/float32/1/yamnet.tflite';
const button = document.querySelector('#run-button');
const select = document.querySelector('#sample');
const fileInput = document.querySelector('#audio-file');
const status = document.querySelector('#status');
const canvas = document.querySelector('#input-canvas');
const context = canvas.getContext('2d');
const body = document.querySelector('#results-body');
const json = document.querySelector('#json-output');
let classifier;
function synthetic(kind) {
const sampleRate = 16000, data = new Float32Array(sampleRate * 2);
let state = 12345;
for (let i = 0; i < data.length; i++) {
if (kind === 'sine') data[i] = .15 * Math.sin(2 * Math.PI * 440 * i / sampleRate);
if (kind === 'noise') { state = (Math.imul(state, 1664525) + 1013904223) >>> 0; data[i] = (state / 4294967296 * 2 - 1) * .15; }
}
return { data, sampleRate, name: `合成 ${kind}(録音データではありません)` };
}
async function readAudio(file) {
if (file.size > 20 * 1024 * 1024) throw new Error('音声ファイルは20 MB以下にしてください');
const audioContext = new AudioContext();
try {
const decoded = await audioContext.decodeAudioData(await file.arrayBuffer());
const length = Math.min(decoded.length, Math.floor(decoded.sampleRate * 15));
const data = new Float32Array(length);
for (let channel = 0; channel < decoded.numberOfChannels; channel++) {
const values = decoded.getChannelData(channel);
for (let i = 0; i < length; i++) data[i] += values[i] / decoded.numberOfChannels;
}
return { data, sampleRate: decoded.sampleRate, name: `${file.name}(冒頭最大15秒・モノラル平均)` };
} finally { await audioContext.close(); }
}
function drawWave(data) {
context.fillStyle = '#12203a'; context.fillRect(0, 0, canvas.width, canvas.height);
context.strokeStyle = '#49edb5'; context.lineWidth = 1.5; context.beginPath();
for (let x = 0; x < canvas.width; x++) {
const index = Math.min(data.length - 1, Math.floor(x / canvas.width * data.length));
const y = canvas.height / 2 - data[index] * canvas.height * .45;
if (x === 0) context.moveTo(x, y); else context.lineTo(x, y);
}
context.stroke();
}
button.addEventListener('click', async () => {
button.disabled = select.disabled = fileInput.disabled = true; body.replaceChildren();
try {
const input = fileInput.files[0] ? await readAudio(fileInput.files[0]) : synthetic(select.value);
if (!input.data.length) throw new Error('音声データが空です');
drawWave(input.data);
document.querySelector('#input-info').textContent = `${input.name} / ${input.sampleRate} Hz / ${(input.data.length / input.sampleRate).toFixed(2)}秒`;
if (!classifier) {
status.textContent = '音声用SDK・WASM・YAMNetモデルを読み込んでいます。';
const { AudioClassifier, FilesetResolver } = await import(`${BASE}/audio_bundle.mjs`);
const files = await FilesetResolver.forAudioTasks(`${BASE}/wasm`);
classifier = await AudioClassifier.createFromOptions(files, { baseOptions: { modelAssetPath: MODEL }, maxResults: 5 });
}
status.textContent = '音声を分類しています。';
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const start = performance.now();
// Float32Arrayと「この配列のサンプルレート」をセットで渡します。
const results = classifier.classify(input.data, input.sampleRate);
const elapsedMs = performance.now() - start;
const categories = results[0]?.classifications[0]?.categories ?? [];
for (const category of categories) {
const row = body.insertRow(); row.insertCell().textContent = category.categoryName || `index ${category.index}`;
row.insertCell().textContent = category.score.toFixed(5);
const meter = document.createElement('meter'); meter.min = 0; meter.max = 1; meter.value = category.score;
row.insertCell().append(meter);
}
json.textContent = JSON.stringify({ sdkVersion: VERSION, model: MODEL, inputName: input.name,
sampleRateHz: input.sampleRate, sampleCount: input.data.length, elapsedMs,
userAgent: navigator.userAgent, results }, null, 2);
status.textContent = `分類完了:${results.length} 区間、${elapsedMs.toFixed(2)} ms。表は最初の区間です。`;
} catch (error) {
console.error(error); status.textContent = `実行できませんでした:${error.message}。対応音声・HTTP起動・通信を確認してください。`;
} finally { button.disabled = select.disabled = fileInput.disabled = false; }
});
select.addEventListener('change', () => { fileInput.value = ''; });
window.addEventListener('pagehide', () => { classifier?.close(); classifier = null; });
音のラベルと時間区間ごとのJSONはtextContentで表示します。合成音を作る処理、ファイルを波形へ変換する処理、classify()を呼ぶ処理を分けて読みます。推論は同期的に実行されるため、async関数で呼ぶだけでは別スレッドへ移りません。
8. 困ったときの確認表
| 症状 | 確認 |
|---|---|
| 音声ファイルをデコードできない | ブラウザ対応形式を確認。小さいWAV等で切り分け、拡張子だけの変更は避けます。 |
| 時間やラベルが不自然 | 波形を作ったsampleRateとclassify()の第2引数が同じか確認します。 |
| 無音に意外なラベルが出る | モデル候補の結果です。録音データの精度評価とは分け、入力と全区間JSONを確認します。 |
| 処理中に操作が止まる | 同期推論です。短い入力で確認し、連続処理はWorker等の設計へ進みます。 |
| SDK・モデルを読み込めない | HTTP起動か、通信できるか、固定URLと版が揃っているか確認します。開発者ツールのConsoleとNetworkで、最初のエラーと失敗した取得先を見ます。 |
問い合わせや自分のメモでは「動かない」だけでなく、使用OS・ブラウザ、選んだ入力、画面の状態、最初のエラー、SDK・モデルの版を残すと切り分けやすくなります。入力内容や私的なファイル名を公開する必要はありません。この記事のキャプチャは実行したサンプルの観察で、すべての端末・入力への品質を保証するものではありません。
参考資料
資料確認日:2026年10月12日。SDKとモデルは別々に更新されます。更新する際は固定した版のサンプルを残し、変更後の入力・結果・実行条件を比べてください。