チュートリアル: 音声レベルの制御
このチュートリアルでは、出力レベルを変更するために音声を処理する方法を示します。これは、低レベルの音声サンプルデータを処理することで実現されます。
レベル: 中級
プラットフォーム: Windows, macOS, Linux
クラス: AudioAppComponent, Random, AudioSourceChannelInfo, AudioBuffer, Slider
はじめに
このチュートリアルは、Tutorial: Build a white noise generatorから続きます。先にそのチュートリアルを読んで理解しておく必要があります。また、Sliderオブジェクトの操作に精通していることも前提としています(Tutorial: The Slider classを参照)。
このチュートリアルのデモプロジェクトをダウンロードしてください: PIP | ZIP。プロジェクトを解凍し、Projucerで最初のヘッダーファイルを開いてください。
この手順でヘルプが必要な場合は、Tutorial: Projucer Part 1: Getting started with the Projucerを参照してください。
デモプロジェクト
このデモプロジェクトは、ホワイトノイズジェネレーターの出力レベルを制御するために使用できる単一のスライダーを含むウィンドウを表示します。これを次のスクリーンショットに示します。

IDE内からプロジェクトを実行して、生成されたホワイトノイズのレベルを実際に制御できることを確認してください。
乗算としてのレベル制御
コードを調べると、MainContentComponentクラスがSlider::Listenerクラスを継承していないことに気付くかもしれません。実際、getNextAudioBlock()関数が呼び出されるとすぐにスライダーの値を取得します:
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
auto level = (float) levelSlider.getValue();
この技術は、このようなシンプルなアプリケーションでは許容されますが、より複雑なアプリケーションでは、ほぼ確実に別の技術を使用する必要があります。オーディオ処理システムを制御する値を、UIコントロールだけに頼ってこれらの値を保存するのではなく、アプリケーションの「データモデル」に保存する方が良い実践です。
getNextAudioBlock()関数でSlider::getValue()を呼び出すことは許容されますが、getNextAudioBlock()関数内でSlider::setValue()関数を呼び出してはいけません。これは、このコードがオーディオスレッドで実行されているためです。オーディオスレッドからUIオブジェクトの状態を変更することは絶対にしてはいけませんが、副作用がないことが確実である限り、UIオブジェクトの状態を照会することは許容されます。
スライダーで指定されたレベルでホワイトノイズを生成するには、基本的な算術演算を実行する必要があります。Random::nextFloat()関数は常に0.0から1.0の間の値を生成します。これを音声波形としてプロットすると、次のようになります:

これにアプローチする簡単な方法は、まずノイズをスケーリングして、常に-1.0から1.0の間でスケーリングされるようにすることです。これを行うには、2.0を掛けて1.0を引きます。これにより、以下に示す信号が得られます:

これは、getNextAudioBlock()関数内のコードで次のように実装されます:
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
auto level = (float) levelSlider.getValue();
for (auto channel = 0; channel < bufferToFill.buffer->getNumChannels(); ++channel)
{
auto* buffer = bufferToFill.buffer->getWritePointer (channel, bufferToFill.startSample);
for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
{
auto noise = random.nextFloat() * 2.0f - 1.0f;
buffer[sample] = noise * level;
}
}
}
軽微な最適化
算術演算の数を減らすことは、DSPの目標であることがよくあります。これは、サンプルごとに1つの乗算演算を回避できる1つのケースです。これを実現するには、まず0.0から1.0の間の値に、必要な出力レベルの2倍に等しい値を掛けます。レベルを0.25にしたいとしましょう。ランダムな値に0.5を掛けると、次のようになります:

最後に、ランダムな値をゼロを中心にオフセットする必要があります。これを行うには、掛けた値の半分に等しい値を引きます。もちろん、これは元のレベル値です。最終結果は次のようになります:

これは、getNextAudioBlock()関数を介してコードで実装できます。サンプルごとの乗算演算が1つ少なくなっていることがわかります:
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
auto level = (float) levelSlider.getValue();
auto levelScale = level * 2.0f;
for (auto channel = 0; channel < bufferToFill.buffer->getNumChannels(); ++channel)
{
auto* buffer = bufferToFill.buffer->getWritePointer (channel, bufferToFill.startSample);
for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
buffer[sample] = random.nextFloat() * levelScale - level;
}
}
この改訂版のコードは、デモプロジェクトのSynthLevelControlTutorial_02.hファイルにあります。
この実装に残る1つの問題は、生成された各オーディオブロックに対してレベルが新しい定数値にのみ更新されることです。この場合、(ソースサウンドがホワイトノイズであるため)聞こえません。これがほとんどの音声コンテンツに適用される場合、この技術はオーディオアーティファクトを導入します。これは、レベル値がオーディオブロック中に滑らかに変化するのではなく、各オーディオブロック間でかなり急激にジャンプする可能性があるためです。これらのアーティファクト(レベルが変化したときの可聴クラックリングなど)を回避する技術は、他のチュートリアルで説明されています(Tutorial: Build a sine wave synthesiserを参照)。
ユーザーインターフェイスに2つ目のスライダーを追加してください。最初のスライダーを使用して左チャンネルのレベルを制御し、この2つ目のスライダーを使用して右チャンネルのレベルを制御します。
まとめ
このチュートリアルでは、音声信号のレベルを制御する方法を示しました。以下のトピックを取り上げました:
- 信号値に必要な出力レベルを掛けることで信号のレベルを変更する。
- スライダーを使用して音声レベルを制御する。