JUCEプログラミング、Draw audio waveforms 4、描画の子コンポーネント化1

Draw audio waveformsチュートリアルにもどってきました。

子コンポーネントについては理解したから進めるね~

JUCEプログラミング、「Draw audio waveforms」チュートリアルに戻ってきました。現状で実装した波形描画のアプリだと、描画部分を「MainComponent」のタイマーを利用して処理しているので、本格的なアプリになったときに、描画処理がユーザーの操作に反応する処理を妨げたりして、アプリの応答性が低下する可能性もあるとのことです。そのため、子コンポーネントとして描画処理を分離して描画処理をアプリの「MainComponent」クラスと機能分離していくような内容となります。

前回までの「Draw audio waveforms」チュートリアルはこちらの記事です。

Draw audio waveforms1

Draw audio waveforms2

今回やっていく内容は、チュートリアルの最後の「Exercise」になります。ZIPでダウンロードできるプログラムとしては、「AudioThumbnailTutorial_03.h」の内容となります。

Exerciseの内容ですので、答えの要素が含まれていることご了承ください。じぶんでチャレンジしたい方は、まだ見ないほうが良いと思います。

このチュートリアルの参考となる「親と子のコンポーネント」に関する記事もご参照ください。

親と子のコンポーネント1

親と子のコンポーネント2

親と子のコンポーネント3

こんな人の役に立つかも

・JUCEチュートリアル「Draw audio waveforms」のExerciseの実装例を知りたい人

目次

プログラムの方向性と概要

ベースとするプログラム

プログラムのベースとして、以前作成した波形表示の音声再生アプリをベースとしました。次のGitHubリポジトリにもアップロードしていますので、ご利用ください。

DrawAudioWave02

子コンポーネントとして、波形を描画していた処理と、現在再生位置の垂直線を描画していた処理をそれぞれ「DrawComponent」というファイルにまとめました。

Projucerで上のスクショのように「.h」「.cpp」を追加します。

※もちろん、mainComponentのファイルのみに実装することも可能ですが、何となく今回は子コンポーネントとして描画関連のクラスを追加していますので、このようにしてみました。

DrawComponent.h

DrawComponent.hファイルに、2つのクラスを追加していきます。

SimpleThumbnailComponentクラス

#include <JuceHeader.h>

#pragma once
class SimpleThumbnailComponent : public juce::Component,
    private juce::ChangeListener
//[1]ChangeListenerを継承します。
{
public:
    //[2]それぞれの関数を定義します。
    SimpleThumbnailComponent(int sourceSamplesPerThumbnailSample,
        juce::AudioFormatManager& formatManager,
        juce::AudioThumbnailCache& cache);
    void setFile(const juce::File& file);
    void paint(juce::Graphics& g) override;
    void paintIfNoFileLoaded(juce::Graphics& g);
    void paintIfFileLoaded(juce::Graphics& g);
    void changeListenerCallback(juce::ChangeBroadcaster* source) override;

private:
    void thumbnailChanged();

    juce::AudioThumbnail thumbnail;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SimpleThumbnailComponent)
};

SimpleThumbnailComponentクラスは、音声波形を描画する処理を担います。

[1]では、「ChangeListener」クラスを継承します。ChangeListenerクラスは、AudioThumbnailクラスの変化を受け取ることができます。そのため、サムネイルが変化したときに描画処理を実行するようなコールバック関数を実行することができるようにしていきます。

[2]では、この後実装していく関数を定義しています。以前MainComponentクラスに実装されていたAudioThumbnail関連の処理をこのクラスに移動させたイメージになります。

「setFile関数」はこのクラスに分離してから追加されました。以前はMainComponentクラスでAudioThumbnailクラスのsetSource関数を直接実行して音声ファイルのthumbnailを読み込みましたが、MainComponentから呼び出してsetFileでこのクラス内のAudioThumbnailオブジェクトのthumbnailへとへファイルをsetSourceするようになりました。

SimplePositionOverlayクラス


class SimplePositionOverlay : public juce::Component,
    private juce::Timer
//[1]
{
public:
    SimplePositionOverlay(const juce::AudioTransportSource& transportSourceToUse);

    void paint(juce::Graphics& g) override;

private:
    void timerCallback() override;

    const juce::AudioTransportSource& transportSource;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SimplePositionOverlay)
};

SimplePositionOverlayクラスは、再生中位置の垂直線を描く機能を持ったクラスです。[1]のように、以前mainComponentクラスで継承していたTimerクラスをこのクラスが代わりに継承して、40msec毎に現在位置の描画を更新していきます。

コンストラクタで、transportSourceを受け取り、その音声ファイルから情報を取得(再生位置など)して40msec毎にpaintで描画する、というシンプルなクラスになっています。

DrawComponent.cpp

先ほどヘッダファイルに定義した関数を「DrawComponent.cpp」へと実装していきます。

SimpleThumbnailComponentクラス

#include "DrawComponent.h"

//SimpleThumbnailComponent
//[1]コンストラクタ
SimpleThumbnailComponent::SimpleThumbnailComponent(int sourceSamplesPerThumbnailSample,
    juce::AudioFormatManager& formatManager,
    juce::AudioThumbnailCache& cache)
    : thumbnail(sourceSamplesPerThumbnailSample, formatManager, cache)
{
    thumbnail.addChangeListener(this);
}

 

まずは、コンストラクタのイニシャライザでprivateなメンバであるAudioThumbnailクラスのオブジェクト「thumbnail」を初期化(読み込み)します。そして、コンストラクタの中でthumbnailの変更を検知するためのコールバック関数を紐づけます。これで音声を読み込むとコールバック関数が実行されるようになりました。

void SimpleThumbnailComponent::setFile(const juce::File& file)
{
    thumbnail.setSource(new juce::FileInputSource(file));
}

setFile関数は、thumbnailに音声ファイルをセットする関数で、これはMainComponentのタイミング(ファイルを指定するタイミング)でセットされます。以前MainComponentクラスで行っていた処理をそのまま引き継いでいます。

void SimpleThumbnailComponent::paint(juce::Graphics& g)
{
    if (thumbnail.getNumChannels() == 0)
        paintIfNoFileLoaded(g);
    else
        paintIfFileLoaded(g);
}

void SimpleThumbnailComponent::paintIfNoFileLoaded(juce::Graphics& g)
{
    g.fillAll(juce::Colours::white);
    g.setColour(juce::Colours::darkgrey);
    g.drawFittedText("No File Loaded", getLocalBounds(), juce::Justification::centred, 1);
}

void SimpleThumbnailComponent::paintIfFileLoaded(juce::Graphics& g)
{
    g.fillAll(juce::Colours::white);

    g.setColour(juce::Colours::red);
    thumbnail.drawChannels(g, getLocalBounds(), 0.0, thumbnail.getTotalLength(), 1.0f);
}

paint関数でも、以前MainComponentで行っていた処理をそのまま移植しているだけになります。

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

void SimpleThumbnailComponent::thumbnailChanged()
{
    repaint();
}

thumbnailの音声データが変更されたタイミングでは、thumbnailChanged関数が呼び出されます。その中身は、repaint関数を呼び出して再描画処理を行うというものです。

SimpolePositionOverlayクラス

//SimpolePositionOverlay
SimplePositionOverlay::SimplePositionOverlay(const juce::AudioTransportSource& transportSourceToUse)
    : transportSource(transportSourceToUse)
{
    startTimer(40);
}

コンストラクタでは、タイマーを起動しています。また、イニシャライザで、AudioTransportSourceを受け取り、クラス内のメンバ変数「transportSouece」へ保管しています。

void SimplePositionOverlay::paint(juce::Graphics& g)
{
    auto duration = (float)transportSource.getLengthInSeconds();

    if (duration > 0.0)
    {
        auto audioPosition = (float)transportSource.getCurrentPosition();
        auto drawPosition = (audioPosition / duration) * (float)getWidth();

        g.setColour(juce::Colours::green);
        g.drawLine(drawPosition, 0.0f, drawPosition, (float)getHeight(), 2.0f);
    }
}

void SimplePositionOverlay::timerCallback()
{
    repaint();
}

paint関数と、timerCallback関数でも、以前MainComponentクラスで行っていた内容をそのまま移植しています。一部変更点としては、子コンポーネントとなったので、MainComponentが指定した位置(の左上)が原点となるので、ウィンドウに対する余白を考慮しない配置計算となりました。drawPositionや、drawLineの位置の数値はよりシンプルに設定できるようになりました。

SimpleThumbnaiComponentとSimplePositionOverlayクラスは、MainComponentのresize関数のところで配置されますが、配置された時点で、上の絵のオレンジのところが原点となります。そのため、MainCoomponent全体に対する位置を考慮する必要がなくなりました。

たしかに、子コンポーネントはGUI的にはとても便利です。

ちょっと長くなってしまいますので、MainComponentのプログラムについては、次回記載していきたいです。

よかったらシェアしてね!
目次
閉じる