メインコンテンツまでスキップ

チュートリアル: 音声波形の描画

📚 Source Page

このチュートリアルでは、AudioThumbnailクラスを使用した音声波形の表示について紹介します。これにより、オーディオアプリケーション内で任意の数の波形を簡単に描画できます。

レベル: 中級
プラットフォーム: Windows, macOS, Linux
クラス: AudioThumbnail, AudioThumbnailCache, AudioFormatReader, ChangeListener

はじめに

ヒント

このチュートリアルは、Tutorial: Build an audio playerから続きます。先にそのチュートリアルを読んで理解しておく必要があります。また、コンポーネント内で描画を行うためのGraphicsクラスとComponent::paint()関数に精通していることも前提としています(Tutorial: The Graphics classを参照)。

このチュートリアルのデモプロジェクトをダウンロードしてください: PIP | ZIP。プロジェクトを解凍し、Projucerで最初のヘッダーファイルを開いてください。

この手順でヘルプが必要な場合は、Tutorial: Projucer Part 1: Getting started with the Projucerを参照してください。

デモプロジェクト

このデモプロジェクトは、Tutorial: Build an audio playerと同じように3つのボタン(サウンドファイルを開く、再生する、停止する)を表示します。

また、サウンドファイルの波形を描画できる矩形領域もあります。デフォルトの状態(サウンドファイルが読み込まれていない)では、アプリケーションは次のように表示されます:

The demo project showing its initial state
The demo project showing its initial state

サウンドファイルが読み込まれると、アプリケーションは次のように表示されます:

The demo project showing a file opened and displayed using the AudioThumbnail class
The demo project showing a file opened and displayed using the AudioThumbnail class

音声波形の描画、特に長いファイルの場合、通常は低解像度バージョンの音声データを、波形の描画を効率的かつユーザーにとって明確にする形式で保存する必要があります。AudioThumbnailクラスは、この低解像度バージョンを処理し、必要に応じて作成および更新されます。

AudioThumbnailのセットアップ

最初の重要なポイントは、AudioThumbnailクラスはComponentクラスのサブクラスではないということです。AudioThumbnailクラスは、別のComponentオブジェクトのpaint()関数内で音声波形の描画を実行するために使用されます。以下のコードは、Tutorial: Build an audio playerのデモプロジェクトに基づいてこの機能を追加する方法を示しています。

追加のオブジェクト

MainContentComponentクラスには、2つのメンバーを追加する必要があります: AudioThumbnailCacheオブジェクトとAudioThumbnailオブジェクトです。AudioThumbnailCacheクラスは、1つ以上の音声ファイルの必要な低解像度バージョンをキャッシュするために使用されます。これは、たとえば、ファイルを閉じて新しいファイルを開き、その後最初のファイルを再度開く場合、AudioThumbnailCacheには最初のファイルの低解像度バージョンがまだ含まれており、データを再スキャンおよび再計算する必要がないことを意味します。もう1つの便利な機能は、AudioThumbnailCacheオブジェクトをAudioThumbnailクラスの異なるインスタンス間で共有できることです。

juce::TextButton openButton;
juce::TextButton playButton;
juce::TextButton stopButton;

std::unique_ptr<juce::FileChooser> chooser;

juce::AudioFormatManager formatManager; // [3]
std::unique_ptr<juce::AudioFormatReaderSource> readerSource;
juce::AudioTransportSource transportSource;
TransportState state;
juce::AudioThumbnailCache thumbnailCache; // [1]
juce::AudioThumbnail thumbnail; // [2]

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

このような静的に割り当てられたオブジェクトを使用する場合、AudioThumbnailCacheオブジェクト[1]がAudioThumbnailオブジェクト[2]の前にリストされていることが重要です。これは、AudioThumbnailコンストラクタへの引数として渡されるためです。また、同じ理由で、AudioFormatManagerオブジェクト[3]がAudioThumbnailオブジェクトの前にリストされていることも重要です。

オブジェクトの初期化

MainContentComponentコンストラクタの初期化リストで、これらのオブジェクトをセットアップします:

MainContentComponent()
: state (Stopped),
thumbnailCache (5), // [4]
thumbnail (512, formatManager, thumbnailCache) // [5]
{
  • [4]: AudioThumbnailCacheオブジェクトは、保存するサムネイルの数を指定して構築する必要があります。
  • [5]: AudioThumbnailオブジェクト自体は、単一のサムネイルサンプルを作成するために使用されるソースサンプル数を指定して構築する必要があります。これにより、低解像度バージョンの解像度が決まります。他の2つの引数は、上述のAudioFormatManagerAudioThumbnailCacheオブジェクトです。

AudioThumbnailクラスは、ChangeBroadcasterクラスの一種でもあります。変更のリスナーとして登録できます[6](MainContentComponentコンストラクタ内で)。これらの変更は、AudioThumbnailオブジェクトが変更され、波形の描画を更新する必要がある場合に発生します。

thumbnail.addChangeListener (this); // [6]

変更への応答

changeListenerCallback()関数では、変更がAudioTransportSourceオブジェクトまたはAudioThumbnailオブジェクトからブロードキャストされているかどうかを判断する必要があります:

void changeListenerCallback (juce::ChangeBroadcaster* source) override
{
if (source == &transportSource)
transportSourceChanged();
if (source == &thumbnail)
thumbnailChanged();
}

transportSourceChanged()関数には、AudioTransportSourceオブジェクトの変更に応答するための元のコードが含まれています:

void transportSourceChanged()
{
changeState (transportSource.isPlaying() ? Playing : Stopped);
}

thumbnailChanged()関数は単純にコンポーネントを再描画します:

void thumbnailChanged()
{
repaint();
}

ファイルを開く

ファイルが正常に開かれたら、それをAudioThumbnailオブジェクトに渡す必要があります。openButtonClicked()関数にコードを追加します[7]:

void openButtonClicked()
{
chooser = std::make_unique<juce::FileChooser> ("Select a Wave file to play...",
juce::File {},
"*.wav");
auto chooserFlags = juce::FileBrowserComponent::openMode
| juce::FileBrowserComponent::canSelectFiles;

chooser->launchAsync (chooserFlags, [this] (const juce::FileChooser& fc) {
auto file = fc.getResult();

if (file != juce::File {})
{
auto* reader = formatManager.createReaderFor (file);

if (reader != nullptr)
{
auto newSource = std::make_unique<juce::AudioFormatReaderSource> (reader, true);
transportSource.setSource (newSource.get(), 0, nullptr, reader->sampleRate);
playButton.setEnabled (true);
thumbnail.setSource (new juce::FileInputSource (file)); // [7]
readerSource.reset (newSource.release());
}
}
});
}

AudioThumbnail::setSource()関数は、InputSourceオブジェクトを受け取ります。この場合、FileInputSourceオブジェクトをファイルから作成します。AudioThumbnailクラスは、このオブジェクトの所有権を引き受けます。これは、波形がバックグラウンドスレッドで作成されるためで、音声ファイルを開く作業をブロックしないようにするためです。ファイルを開く作業が完了すると、AudioThumbnailオブジェクトは変更をブロードキャストし、コンポーネントが再描画されます。

波形の描画

まず、MainContentComponentクラスのpaint()関数全体を見てみましょう:

void paint (juce::Graphics& g) override
{
g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));

auto thumbnailBounds = getLocalBounds().removeFromBottom (getHeight() - 100);

if (thumbnail.getNumChannels() == 0)
paintIfNoFileLoaded (g, thumbnailBounds);
else
paintIfFileLoaded (g, thumbnailBounds);
}

これは、ヘッダー領域を残すために、thumbnailBounds変数を取得して、コンポーネントの境界から下部の100ピクセルを引いた矩形を取得します。次に、AudioThumbnailオブジェクトにファイルがロードされているかどうかに応じて、適切なヘルパー関数を呼び出します。ファイルがロードされていない場合、paintIfNoFileLoaded()関数は背景を描画し、何もロードされていないことをユーザーに伝えるテキストを描画します[8]:

void paintIfNoFileLoaded (juce::Graphics& g, const juce::Rectangle<int>& thumbnailBounds)
{
g.setColour (juce::Colours::darkgrey);
g.fillRect (thumbnailBounds);
g.setColour (juce::Colours::white);
g.drawFittedText ("No File Loaded", thumbnailBounds, juce::Justification::centred, 1); // [8]
}

そして、ファイルがロードされている場合、paintIfFileLoaded()関数は、AudioThumbnail::drawChannels()関数を呼び出して波形を描画します[9]:

void paintIfFileLoaded (juce::Graphics& g, const juce::Rectangle<int>& thumbnailBounds)
{
g.setColour (juce::Colours::white);
g.fillRect (thumbnailBounds);

g.setColour (juce::Colours::red);

auto audioLength = (float) thumbnail.getTotalLength();
thumbnail.drawChannels (g, // [9]
thumbnailBounds,
0.0,
audioLength,
1.0f);
}

AudioThumbnail::drawChannels()関数への引数は次のとおりです:

  • 描画先のGraphicsオブジェクト。
  • 描画先の矩形領域。
  • オーディオファイルのタイムライン上の開始時間(秒単位)。
  • オーディオファイルのタイムライン上の終了時間(秒単位)。
  • チャンネル間の垂直ズーム係数。値が1.0の場合、波形は矩形を最大限に埋めます。1.0未満の場合、波形は矩形よりも小さく表示され、1.0より大きい場合、矩形から垂直方向にクリップされます。
注記

演習: 実際には、サウンドファイルの特定の領域のみを表示したい場合が一般的です。AudioThumbnail::drawChannels()関数から、JUCEを使用してこれを実装することがいかに簡単かがわかるはずです。ファイルの特定の領域のみを表示するようにコードを変更してみてください。

時間位置マーカーの追加

このセクションでは、ファイル再生の現在の時間位置を示す垂直線を表示に追加する手順を説明します。

タイマーの追加

まず、Timerクラスを基底クラスのリストに追加する必要があります[10]:

class MainContentComponent : public juce::AudioAppComponent,
public juce::ChangeListener,
private juce::Timer // [10]
{
public:

次に、タイマーコールバックでコンポーネントを再描画する必要があります。Timerクラスからprivateに継承したことに注意してください。このコードは必ずprivateセクションに追加してください:

void timerCallback() override
{
repaint();
}

MainContentComponentコンストラクタでタイマーを開始する必要があります[11] --- 40msごとで十分です:

startTimer (40); // [11]
}
注記

実際には、ファイルが正常に開かれたらタイマーを開始することで、タイマーの開始を遅らせることができます。

位置線の描画

最後に、線を描画するために、線の位置を計算し、サムネイルの描画に描画する必要があります:

void paintIfFileLoaded (juce::Graphics& g, const juce::Rectangle<int>& thumbnailBounds)
{
g.setColour (juce::Colours::white);
g.fillRect (thumbnailBounds);

g.setColour (juce::Colours::red);

auto audioLength = (float) thumbnail.getTotalLength(); // [12]
thumbnail.drawChannels (g, thumbnailBounds, 0.0, audioLength, 1.0f);

g.setColour (juce::Colours::green);

auto audioPosition = (float) transportSource.getCurrentPosition();
auto drawPosition = (audioPosition / audioLength) * (float) thumbnailBounds.getWidth()
+ (float) thumbnailBounds.getX(); // [13]
g.drawLine (drawPosition, (float) thumbnailBounds.getY(), drawPosition, (float) thumbnailBounds.getBottom(), 2.0f); // [14]
}
  • [12]: この値を2回使用する必要があるため、ファイルの長さを変数に格納します。
  • [13]: 位置は、音声ファイルの全長に対する割合として計算されます。線を描画する位置は、サムネイルが描画される矩形の幅に対する同じ割合に基づく必要があります。矩形のx座標に基づいて描画位置をオフセットする必要があります。
  • [14]: ここでは、矩形の上部(y)と下部の間に幅2ピクセルの線を描画します。

以上です。これでアプリケーションをビルドして実行できます。

ヒント

このアプリケーションの変更版のソースコードは、デモプロジェクトのAudioThumbnailTutorial_02.hファイルにあります。

警告

この例の問題は、40msごとにコンポーネントを強制的に再描画することです。これは簡単な例では許容されるかもしれませんが、より複雑なケースではパフォーマンスの問題に直面する可能性があります。これについては、以下の演習を参照してください。

注記

演習: 描画を別の子コンポーネントに分離してください(Tutorial: Parent and child componentsを参照)。3つのコンポーネントが必要です:

  • 音声波形を描画するコンポーネント。
  • 再生位置を垂直線として描画するコンポーネント。
  • これら2つの子コンポーネントを含むメイン親コンポーネント(互いに重ねて配置)。

これにより、コードがわかりやすくなるだけでなく、正しく行えば、毎フレーム波形を再描画することを避けられるため、はるかに効率的になります。ユーザーが波形をクリックしたときに再生位置を変更する機能も追加できます。

ヒント

この演習の可能な実装のソースコードは、デモプロジェクトのAudioThumbnailTutorial_03.hおよびAudioThumbnailTutorial_04.hファイルにあります。

まとめ

このチュートリアルでは、AudioThumbnailクラスとそれをオーディオアプリケーションに統合する方法を紹介しました。特に以下について取り上げました:

  • AudioThumbnailAudioThumbnailCacheオブジェクトの初期化。
  • コンポーネント内でのAudioThumbnailクラスの使用。
  • 描画が複雑なコンテンツが不必要に再描画されないようにコンポーネントを構造化する。

関連項目