0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

音に合わせて動くビジュアライザーを作るとき、最初に必要になるのは「音を数値として取り出すこと」です。

たとえば、

  • 音量が大きいときに画面を光らせる
  • キックやベースに合わせてオブジェクトを膨らませる
  • ハイハットやノイズに合わせて細かい粒子を動かす
  • 波形を使って線やパーティクルを揺らす

といった表現を作るには、音声ファイルをただ再生するだけでは足りません。

そこで今回は、Web Audio API を使って、

  • volume
  • bass
  • mids
  • highs
  • waveform

を取得する最小実装を作ってみます。

作るもの

ローカルの音声ファイルを読み込んで、ブラウザ上で解析します。

最終的には、以下のような値を取得できるようにします。

type AudioFeatures = {
  volume: number
  bass: number
  mids: number
  highs: number
  waveform: Float32Array
}

それぞれの意味は以下です。

内容
volume 音全体の大きさ
bass 低域の強さ
mids 中域の強さ
highs 高域の強さ
waveform 波形データ

音声ビジュアライザーを作る場合、まずこの形まで持っていけると、その後の3D表現やUIに使いやすくなります。

全体の流れ

Web Audio API では、ざっくり以下のような流れで音を解析します。

audio element
  ↓
AudioContext
  ↓
MediaElementSource
  ↓
AnalyserNode
  ↓
time domain data / frequency data
  ↓
volume / bass / mids / highs / waveform

重要なのは AnalyserNode です。

AnalyserNode を使うと、再生中の音声から時間領域データと周波数領域データを取得できます。

HTML側

まずは音声ファイルを選択する input と、再生用の audio 要素を用意します。

<input id="fileInput" type="file" accept="audio/*" />
<audio id="audio" controls></audio>

<pre id="output"></pre>

選択した音声ファイルは、サーバーにアップロードせず、ブラウザ上で object URL として扱います。

const fileInput = document.querySelector<HTMLInputElement>('#fileInput')
const audio = document.querySelector<HTMLAudioElement>('#audio')

fileInput?.addEventListener('change', () => {
  const file = fileInput.files?.[0]

  if (!file || !audio) {
    return
  }

  const url = URL.createObjectURL(file)
  audio.src = url
})

これで、ローカルの音源をブラウザ上で再生できます。

AudioContextを作る

次に AudioContext を作成します。

const audioContext = new AudioContext()
const source = audioContext.createMediaElementSource(audio)
const analyser = audioContext.createAnalyser()

source.connect(analyser)
analyser.connect(audioContext.destination)

createMediaElementSource(audio) によって、HTMLAudioElement の音を Web Audio API の処理対象にできます。

その後、AnalyserNode につないで、さらに destination に接続します。

audio
  ↓
source
  ↓
analyser
  ↓
speakers

この接続を忘れると、音が鳴らなかったり、解析できなかったりします。

AnalyserNodeの設定

AnalyserNode にはいくつか設定があります。

analyser.fftSize = 2048
analyser.smoothingTimeConstant = 0.8

fftSize は周波数解析の細かさに関わります。

値を大きくすると細かく取れますが、そのぶん処理も重くなります。

smoothingTimeConstant は値のなめらかさです。

音に対して細かく反応させたい場合は低め、ゆったり反応させたい場合は高めにします。

データ用の配列を作る

時間領域データと周波数領域データを取得するために、配列を用意します。

const frequencyData = new Uint8Array(analyser.frequencyBinCount)
const timeDomainData = new Uint8Array(analyser.fftSize)

周波数データは getByteFrequencyData で取得します。

analyser.getByteFrequencyData(frequencyData)

時間領域データは getByteTimeDomainData で取得します。

analyser.getByteTimeDomainData(timeDomainData)

volumeを計算する

音量は、時間領域データから RMS を使って計算します。

function calculateVolume(timeDomainData: Uint8Array): number {
  let sum = 0

  for (const value of timeDomainData) {
    const normalized = (value - 128) / 128
    sum += normalized * normalized
  }

  return Math.sqrt(sum / timeDomainData.length)
}

getByteTimeDomainData で取得できる値は 0〜255 です。

中心が 128 なので、そこから引いて -1〜1 くらいの範囲に正規化します。

その後、二乗平均平方根を取ることで、音量のような値にできます。

周波数帯域を分ける

次に、低域・中域・高域を取ります。

今回は以下のように分けます。

bass: 20Hz - 250Hz
mids: 250Hz - 2000Hz
highs: 2000Hz - 8000Hz

まず、周波数から配列の index に変換する関数を作ります。

function frequencyToIndex(
  frequency: number,
  sampleRate: number,
  fftSize: number
): number {
  return Math.round((frequency / (sampleRate / 2)) * (fftSize / 2))
}

次に、指定した周波数帯域の平均値を取得します。

function getBandEnergy(
  frequencyData: Uint8Array,
  sampleRate: number,
  fftSize: number,
  minHz: number,
  maxHz: number
): number {
  const startIndex = frequencyToIndex(minHz, sampleRate, fftSize)
  const endIndex = frequencyToIndex(maxHz, sampleRate, fftSize)

  let sum = 0
  let count = 0

  for (let i = startIndex; i <= endIndex; i++) {
    sum += frequencyData[i] ?? 0
    count++
  }

  if (count === 0) {
    return 0
  }

  return sum / count / 255
}

frequencyData の値は 0〜255 なので、最後に 255 で割って 0〜1 に近い値にしています。

bass / mids / highs を取得する

先ほどの関数を使うと、各帯域はこのように取得できます。

const bass = getBandEnergy(
  frequencyData,
  audioContext.sampleRate,
  analyser.fftSize,
  20,
  250
)

const mids = getBandEnergy(
  frequencyData,
  audioContext.sampleRate,
  analyser.fftSize,
  250,
  2000
)

const highs = getBandEnergy(
  frequencyData,
  audioContext.sampleRate,
  analyser.fftSize,
  2000,
  8000
)

これで、音をざっくり低域・中域・高域に分けられます。

音楽的には、

  • bass: キック、サブベース、低音
  • mids: ボーカル、シンセ、コード、メロディ
  • highs: ハイハット、ノイズ、クリック感

のような反応に使えます。

waveformを作る

波形データは、時間領域データを -1〜1 の値に変換して作ります。

function createWaveform(timeDomainData: Uint8Array): Float32Array {
  const waveform = new Float32Array(timeDomainData.length)

  for (let i = 0; i < timeDomainData.length; i++) {
    waveform[i] = ((timeDomainData[i] ?? 128) - 128) / 128
  }

  return waveform
}

この waveform は、線を描いたり、パーティクルを揺らしたりするのに使えます。

音量だけで動かすと単調になりやすいですが、波形を混ぜると細かい揺れが出しやすくなります。

解析ループを作る

最後に、requestAnimationFrame で毎フレーム解析します。

function analyze() {
  analyser.getByteFrequencyData(frequencyData)
  analyser.getByteTimeDomainData(timeDomainData)

  const volume = calculateVolume(timeDomainData)

  const bass = getBandEnergy(
    frequencyData,
    audioContext.sampleRate,
    analyser.fftSize,
    20,
    250
  )

  const mids = getBandEnergy(
    frequencyData,
    audioContext.sampleRate,
    analyser.fftSize,
    250,
    2000
  )

  const highs = getBandEnergy(
    frequencyData,
    audioContext.sampleRate,
    analyser.fftSize,
    2000,
    8000
  )

  const waveform = createWaveform(timeDomainData)

  const features = {
    volume,
    bass,
    mids,
    highs,
    waveform,
  }

  console.log(features)

  requestAnimationFrame(analyze)
}

analyze()

これで、再生中の音からリアルタイムに特徴量を取得できます。

自動再生制限に注意する

Web Audio API を使うときに注意したいのが、ブラウザの自動再生制限です。

ユーザー操作なしに AudioContext を開始しようとすると、うまく動かないことがあります。

再生ボタンなど、ユーザー操作のタイミングで resume() するようにしておくと安全です。

await audioContext.resume()
await audio.play()

音声系のWebアプリで「音が鳴らない」「解析が始まらない」ときは、まずここを疑うとよいです。

まとめ

今回は、Web Audio API を使って、

  • volume
  • bass
  • mids
  • highs
  • waveform

を取得する最小実装を作りました。

音声ビジュアライザーを作るときは、最初から派手な表現を作ろうとするよりも、まず音を数値として扱えるようにするのが大事です。

音を再生する
↓
AnalyserNodeで解析する
↓
volume / bass / mids / highs / waveform に分ける
↓
UIや3D表現に使う

この流れができると、あとは Three.js や Canvas、SVG などに渡して、音に反応する表現を作れるようになります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?