7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Oscillator のカスタム波形を作った際の音量調整

Last updated at Posted at 2015-09-17

波形から PeriodicWave を作る

Web Audio API において Oscillator でカスタム波形を作るには PeriodicWave を作って、それを割り当てる必要があります。
PeriodicWave を作るには倍音構成を表す 2 つの配列で渡す必要があるため、波形から作る際は離散フーリエ変換(DFT)を通さなければなりません。
例えば以下のようになります。ここでは DFT ではなく FFT を使用していますが、意味は同じです。

// 波形(ここでは簡単にするために sine 波形)
const buffer = new Float32Array(2048);
for(let i=0, l=buffer.length; ++i) {
    buffer[i] = Math.sin( Math.PI * 2 * i / 2048 );
}

// 離散フーリエ変換で倍音構成に変換
const fdata = fft(buffer);

// PeriodicWave を作る
const ctx = new AudioContext();
const periodic = ctx.createPeriodicWave(...fdata);


// 離散フーリエ変換をする函数
function fft(input) {
    let n = input.length, theta = 2 * Math.PI / n,
        ar = new Float32Array(n), ai = new Float32Array(n),
        m, mh, i, j, k, irev,
        wr, wi, xr, xi,
        cos = Math.cos, sin = Math.sin;

    for(i=0; i<n; ++i) {
        ar[i] = input[i];
    }

    // scrambler
    i=0;
    for(j=1; j<n-1; ++j) {
        for(k = n>>1; k>(i ^= k); k>>=1);
        if(j<i) {
            xr = ar[j];
            xi = ai[j];
            ar[j] = ar[i];
            ai[j] = ai[i];
            ar[i] = xr;
            ai[i] = xi;
        }
    }
    for(mh=1; (m = mh << 1) <= n; mh=m) {
        irev = 0;
        for(i=0; i<n; i+=m) {
            wr = cos(theta * irev);
            wi = sin(theta * irev);
            for(k=n>>2; k > (irev ^= k); k>>=1);
            for(j=i; j<mh+i; ++j) {
                k = j + mh;
                xr = ar[j] - ar[k];
                xi = ai[j] - ai[k];
                ar[j] += ar[k];
                ai[j] += ai[k];
                ar[k] = wr * xr - wi * xi;
                ai[k] = wr * xi + wi * xr;
            }
        }
    }
    
    // remove DC offset
    ar[0] = ai[0] = 0;
        
    return [ar, ai];
}

// 3秒間再生する函数
function play() {
	const osc = ctx.createOscillator();
	osc.setPeriodicWave(periodic);
	osc.connect(ctx.destination);

	osc.start(ctx.currentTime);
	osc.stop(ctx.currentTime + 3.0);
}

(FFT のコードは http://www.kuma-de.com/blog/2008-08-31/82 を参考)

音量の問題

上記のように離散フーリエ変換を使い PeriodicWave を作って割り当てた Oscillator と、元の波形を SourceBuffer を使って再生した場合とでは音量が異なります。これは PeriodicWave の Wave Generation が

x(t) = \sum_{k=1}^{L-1} \left(a[k]\cos2\pi k t + b[k]\sin2\pi k t\right)

(http://webaudio.github.io/web-audio-api/#waveform-generation より引用)

と定義されているのですが、この振幅のスケーリングと音量の関係性についての規定がないため、各ブラウザの実装依存だと考えられます。

またもう 1 つの要因として PeriodicWave はデフォルトで標準化をしており、これも音量に影響を与えます。まず逆離散フーリエ変換で元の波形の形にし、そしてその最大値で割ることで標準化しています。

\tilde{x}(n) = \sum_{k=1}^{L-1} \left(a[k]\cos\frac{2\pi k n}{N} + b[k]\sin\frac{2\pi k n}{N}\right) \\\
f = \max_{n = 0, \ldots, N - 1} |\tilde{x}(n)| \\\
\hat{x}(n) = \frac{\tilde{x}(n)}{f}

(http://webaudio.github.io/web-audio-api/#waveform-normalization より引用)

よって Oscillator の音量を BufferSource に合わせる方法について考えます。

2つの解決方法

解決するには実験と検証を繰り返すしかありませんが、その結果以下の 2 つの方法がそれっぽい音量になるみたいです。

disableNormalization フラグを使う場合

最新の Web Audio API の仕様では PeriodicWave の標準化をとめることができ、今のブラウザだと Google Chrome Canary で試すことが出来ます。

ctx.createPeriodicWave(real, imag, {disableNormalization: true});

このとき離散フーリエ変換の正規化係数を以下のように 2/N にすると音量が合います。

fft.js
// 離散フーリエ変換をする函数 (2/N倍している)
function fft(input) {
    let n = input.length, theta = 2 * Math.PI / n,
        ar = new Float32Array(n), ai = new Float32Array(n),
        m, mh, i, j, k, irev,
        wr, wi, xr, xi,
        cos = Math.cos, sin = Math.sin;

    for(i=0; i<n; ++i) {
        ar[i] = input[i];
    }

    // scrambler
    i=0;
    for(j=1; j<n-1; ++j) {
        for(k = n>>1; k>(i ^= k); k>>=1);
        if(j<i) {
            xr = ar[j];
            xi = ai[j];
            ar[j] = ar[i];
            ai[j] = ai[i];
            ar[i] = xr;
            ai[i] = xi;
        }
    }
    for(mh=1; (m = mh << 1) <= n; mh=m) {
        irev = 0;
        for(i=0; i<n; i+=m) {
            wr = cos(theta * irev);
            wi = sin(theta * irev);
            for(k=n>>2; k > (irev ^= k); k>>=1);
            for(j=i; j<mh+i; ++j) {
                k = j + mh;
                xr = ar[j] - ar[k];
                xi = ai[j] - ai[k];
                ar[j] += ar[k];
                ai[j] += ai[k];
                ar[k] = wr * xr - wi * xi;
                ai[k] = wr * xi + wi * xr;
            }
        }
    }
    
    // remove DC offset
    ar[0] = ai[0] = 0;
    
    // *** 追加 ***
    for(i=0; i<n; ++i) {
        ar[i] *= 2 / n;
        ai[i] *= 2 / n;
    }
    
    return [ar, ai];
}

Gain を使う方法

離散フーリエ変換する前の波形の振幅の 2 倍の値を割り当てた Gain を Oscillator に繋げるやり方です。
この方法だと disableNormalization をサポートしていないブラウザでも大丈夫です。

// 波形の振幅を得る
let amplitude = 0;
for(let i=0, temp; i<buffer.length; ++i) {
	temp = Math.abs(buffer[i]);
	if(temp > amplitude)	amplitude = temp;
}

// 3秒間再生する函数
function Play() {
	const osc = ctx.createOscillator(), gainNode = ctx.createGain();
	osc.setPeriodicWave(periodic);
	osc.connect(gainNode);

	gainNode.gain.value = 2 * amplitude;
	gainNode.connect(ctx.destination);

	osc.start(ctx.currentTime);
	osc.stop(ctx.currentTime + 3.0);
}

2つの解決方法の等価性

Gain を使う方法を見てもらうと唐突に波形を 2 倍していると思われるのですが、この 2 つの解決方法は等価です。

元の波形の配列(数列)を A[n] と定義すると、Gain を使う方法で得た amplitude は以下のように定義されます。

amplitude := \max_{n = 0, \ldots, N - 1} | \ A[n] \ | \ \cdots \! \cdots (1)

一方で disableNormalization フラグを使う方法において、離散フーリエ変換の正規化係数は 2/N で、音量の問題で指摘したように PeriodicWave の標準化で使われる逆離散フーリエ変換では正規化係数が 1 となっています。よって波形を離散フーリエ変換 FN、逆離散フーリエ変換 FN-1 に順に適用させると、以下のようになります。
ただし単純に波形を順に適用させた場合、波形に N をかけたことと恒等的に等しいことと、離散フーリエ変換、逆離散フーリエ変換は波形を低数倍した時、その定数を外にだせることを使っています。

\begin{eqnarray}
F_N^{-1} ( \ F_N ( \ A[n] \ ) \ ) &=& N \cdot 1 \cdot \frac{2}{N} A[n] \\
                                  &=& 2 \ A[n]
\end{eqnarray}

従って、この時の波形の振幅 f は

\begin{eqnarray}
f &=& \max_{n = 0, \ldots, N - 1} | \ 2 \ A[n] \ | \\
  &=& 2 \cdot \max_{n = 0, \ldots, N - 1} | \ A[n] \ | \ \cdots \! \cdots (2)
\end{eqnarray}

(1), (2) より

f = 2 \cdot amplitude

よって Gain を使う方法は disableNormalization フラグを使う方法に対して、波形の振幅を合わせることで音量を同じにしていると言えるので、2 つの解決方法は等価です。

関連リンク

jsdo.it に書いた実験、検証用コード
http://jsdo.it/Moriken/sfDX/

Stack Overflow に投稿した質問
http://stackoverflow.com/questions/32402804/play-buffer-by-periodicwave-in-web-audio-api-can-i-set-gain-as-loud-as-buffer

7
3
1

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
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?