🐡

Tracktion Engineことはじめ ~DAWを作ろう~

2024/12/28に公開

この記事はJUCE Advent Calendar 2024の12/24の記事です.

はじめに

オーディオプログラマーなら誰もが一度は思う(?)オリジナルのDAWを作りたいという欲を満たすべく,Tracktion Engineに入門てみました.

Tracktion Engineとは?

https://github.com/Tracktion/tracktion_engine では以下のように説明されています.また,macOS, Windows, Linux, iOS, Androidに対応しています.

The aim of Tracktion Engine is to provide a high level data model and set of classes for building sequence based audio applications. You can build anything from a simple file-player or sequencer to a full blown DAW.
Tracktion Engineの目的は,シーケンスベースのオーディオアプリケーションを構築するための高レベルのデータモデルとクラスを提供することです.シンプルなファイルプレーヤーやシーケンサからフル機能のDAWまで,何でも構築できます.

JUCEの開発者として有名なJulian Storer氏が始めたプロジェクトで, 現在は開発に関わっていないようですが, (コミットログを見る限り全然関わっていました.むしろ2020年ごろからはJUCEの開発に関わっていないようです.*1) tracktin_engine内でもJUCEを使用しており,JUCEとの親和性が高いです.

Tracktion Engineの詳しい解説については,こちらを参照.

作ったもの

JUCEとTracktion Engineを使って,簡単なオーディオプレイヤーを作成しました.
オーディオファイルを読み込んで再生するだけのシンプルなアプリケーションです.

https://bsky.app/profile/ph3nac.bsky.social/post/3lecclewg3c2k

https://github.com/ph3nac/tiny-daw/tree/audio_play_back_demo

プロジェクト構成

src/
│
├── Main.cpp               // アプリケーションのエントリーポイント
├── MainComponent.h        // メインコンポーネントのGUIとロジック
├── AudioTrackComponent.h  // オーディオトラックのGUI管理
├── Thumbnail.h            // 波形表示クラス
└── Utils.h                // ユーティリティ関数

Thumbnail.hUtils.hはTracktion Engineの公式サンプルから拝借したものを修正して利用しています.

Tracktion Engine 主要クラス

  • traction::Engine
  • tracktion::Edit
  • tracktion::TransportControl
  • traction::Track
  • traction::Clip

クラスの役割

  • Engine: アプリケーション全体の管理を行い,エディットやオーディオ処理を統括する.いろんなメソッドの引数に渡すことが多い.
  • Edit: エンジンの中で楽曲全体を管理するクラス.複数のトラックやクリップを保持する.getTransport()メソッドでTransportControlを取得して取り回すことが多い.
  • Track: Edit内に配置される.本記事では,AudioTrackを使用.
  • Clip: オーディオデータやMIDIデータを保持するクリップ.本記事では,WaveAudioClipを使用してオーディオデータを保持.
  • TransportControl: 再生・停止などのトランスポート操作を担当する.

各コンポーネントの解説

MainComponent (オーディオの再生・停止などの操作)

このコンポーネントでは主に以下の処理を行っています.

  • tracktion::Engineの初期化
  • tracktion::Editの作成
  • 再生・停止ボタンでの再生・停止の切り替え
  • editへのAudioTrackの追加
class MainComponent : public juce::Component, private juce::ChangeListener
{
public:
    MainComponent()
    {

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

private:
    // Tracktion Engineの初期化
    te::Engine engine { "TinyDAW" };
    // Editを作成
    te::Edit edit { engine, te::Edit::EditRole::forEditing };
    // TransportControlを取得
    te::engine::TransportControl& transport { edit.getTransport() };

    juce::FileChooser audioFileChooser { "select an audio file",
                                         engine.getPropertyStorage().getDefaultLoadSaveDirectory ("TinyDAW"),
                                         "*.wav"};
    std::unique_ptr<juce::TemporaryFile> defaultTempProject;

    juce::TextButton playPauseButton { "Play" };
    juce::TextButton loadFileButton { "Load File" };
    AudioTrackComponent audioTrackViewComponent { transport };

    // transportを使用して再生・停止を切り替える
    void togglePlay()
    {
        if (transport.isPlaying())
        {
            transport.stop (false, false);
        }
        else
        {
            // transport.playFromStart (true);
            transport.play (false);
        }
    }

    // トラックの最初のクリップを取得
    te::WaveAudioClip::Ptr getClip()
    {
        if (auto track = Utils::getOrInsertAudioTrackAt (edit, 0))
        {
            // track->getClips()はClipの配列を返すため,dynamic_castでWaveAudioClipに変換す必要がある
            if (auto clip = dynamic_cast<te::WaveAudioClip*> (track->getClips()[0]))
            {
                return clip;
            }
        }
        return {};
    }

    // クリップからファイルを取得
    juce::File getSourceFile()
    {
        if (auto clip = getClip())
        {
            return clip->getAudioFile().getFile();
        }
        return {};
    }

    void updatePlayButtonText()
    {
        playPauseButton.setButtonText (transport.isPlaying() ? "Pause" : "Play");
    }

    // 再生・停止ボタンの表示を更新する
    void changeListenerCallback (juce::ChangeBroadcaster*) override
    {
        updatePlayButtonText();
    }
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

AudioTrackComponent (オーディオトラックの表示と操作)

AudioTrackComponentではオーディオファイルを読み込み,波形表示を行っています.
オーディオファイルからClipを作成しTrackへ追加する処理は,後述するUtils::loadAudioFileAsClip()内部で行っています.

class AudioTrackComponent : public juce::Component
{
public:
    AudioTrackComponent (te::engine::TransportControl& tc) : transport (tc), thumbnail (transport)
    {
        
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    void setFile (const juce::File& file)
    {
        // オーディオファイルを読み込み、クリップを作成して波形表示を更新する
        if (auto clip = Utils::loadAudioFileAsClip (transport.edit, file))
        {
            clip->setAutoPitch (false);
            clip->setAutoTempo (false);
            clip->setTimeStretchMode (te::TimeStretcher::defaultMode);
            thumbnail.setFile (Utils::loopAroundClip (*clip)->getPlaybackFile());
        }
        else
        {
            // サムネイルを削除
            thumbnail.setFile ({ transport.engine });        }
    }

private:
    te::engine::TransportControl& transport;
    Thumbnail thumbnail;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioTrackComponent)
};

Thumbnail (波形と再生位置カーソル表示)

Thumbnailクラスでは,オーディオファイルの波形表示と再生位置カーソルの表示を行っています.
また,再生位置カーソルをドラッグして再生位置を変更する処理も実装しています.

struct Thumbnail : public juce::Component
{
    explicit Thumbnail (te::TransportControl& tc) : transport (tc)
    {

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    // smartThumbnailに新しいファイルを設定
    void setFile (const te::AudioFile& file)
    {
        smartThumbnail.setNewFile (file);
        // 一定時間ごとに再生位置カーソルを更新
        cursorUpdater.startTimerHz (25);
        repaint();
    }

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    // 再生位置カーソルをドラッグで移動
    void mouseDown (const juce::MouseEvent& e) override
    {
        // transportを使ってドラッグ中は再生を停止
        transport.stop (false, false);
        transport.setUserDragging (true);
        mouseDrag (e);
    }

    // マウスをドラッグして再生位置を変更
    void mouseDrag (const juce::MouseEvent& e) override
    {
        if (! e.mouseWasDraggedSinceMouseDown())
            return;

        jassert (getWidth() > 0);
        const float proportion = e.position.x / getWidth();
        // transportを使って再生位置を変更
        transport.setPosition (toPosition (transport.getLoopRange().getLength()) * proportion);
    }

    void mouseUp (const juce::MouseEvent& e) override
    {
        transport.setUserDragging (false);

        if (e.mouseWasDraggedSinceMouseDown())
            return;
    }

private:
    te::TransportControl& transport;
    te::SmartThumbnail smartThumbnail {
        transport.engine,
        te::AudioFile (transport.engine), // 初期化
        *this,
        nullptr
    };
    // 再生位置カーソル
    juce::DrawableRectangle cursor, pendingCursorTo, pendingCursorAt;
    // 再生位置カーソルの更新用タイマー
    te::LambdaTimer cursorUpdater;

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Utils (オーディオファイルの読み込みとクリップ作成など)

OSのファイルダイアログを開いてオーディオファイルを読み込む処理や,
オーディオファイルを読み込んでWaveAudioClipとして追加する処理をまとめたユーティリティ関数をUtils.hにまとめています.
Utils::loadAudioFileAsClip()ないでは,index=0のオーディオトラックにオーディオファイルを読み込んでWaveAudioClipとして追加していますが,indexを変更することでマルチトラックの機能も利用できます.

namespace Utils
{

// オーディオファイルを読みこむ
inline void browseForAudioFile (
    tracktion::Engine& engine,
    std::function<void (const juce::File&)> fileChosenCallback)
{
    auto fileChooser = std::make_shared<juce::FileChooser> (
        "Choose an audio file",
        engine.getPropertyStorage().getDefaultLoadSaveDirectory ("TinyDAW"),
        engine.getAudioFileFormatManager()
            .readFormatManager.getWildcardForAllFormats());
    fileChooser->launchAsync (
        juce::FileBrowserComponent::openMode + juce::FileBrowserComponent::canSelectFiles,
        [fileChooser, &engine, callback = std::move (fileChosenCallback)] (const juce::FileChooser&)
        {
            const auto file = fileChooser->getResult();
            if (file.existsAsFile())
            {
                engine.getPropertyStorage().setDefaultLoadSaveDirectory (
                    "TinyDaw", file.getParentDirectory());
            }
            callback (file);
        });
}

// オーディオトラックを作成する
inline tracktion::AudioTrack* getOrInsertAudioTrackAt (tracktion::Edit& edit,
                                                       int index)
{
    // index番目のオーディオトラックが存在しない場合は追加する
    edit.ensureNumberOfAudioTracks (index + 1);
    return tracktion::getAudioTracks (edit)[index];
}

inline void removeAllClips (tracktion::AudioTrack* track)
{
    auto clips = track->getClips();
    for (auto* clip : clips)
    {
        clip->removeFromParent();
    }
}

// オーディオファイルを読み込んでWaveAudioClipとして追加する
inline tracktion::WaveAudioClip::Ptr loadAudioFileAsClip (
    tracktion::Edit& edit,
    const juce::File& file)
{
    if (auto* track = tracktion::getOrInsertAudioTrackNearestIndex (edit, 0))
    {
        removeAllClips (track);

        tracktion::AudioFile audioFile { edit.engine, file };

        if (audioFile.isValid())
        {
            // クリップをtrackに追加する..
            if (auto newClip = track->insertWaveClip (
                    file.getFileNameWithoutExtension(), file, { { {}, tracktion::TimeDuration::fromSeconds (audioFile.getLength()) }, {} }, false))
            {
                return newClip;
            }
        }
    }
    return {};
}

// editのtransportControlを使ってクリップをループ再生する
template <typename ClipType>
typename ClipType::Ptr loopAroundClip (ClipType& clip)
{
    auto& transport = clip.edit.getTransport();
    transport.setLoopRange (clip.getEditTimeRange());
    transport.looping = true;
    transport.setPosition (std::chrono::seconds (0));
    return clip;
}

} // namespace Utils

まとめ

全体的にシーケンサーエンジンとして必要な機能を抽象化したAPIが用意されており,個人でDAWを作る際の現実的な選択肢となりそうです.

今後のロードマップ

tracktion_engineにはオーディオファイルの再生に限らず,MIDIデータを扱う機能や,プラグインホストとしての機能も備えています.もちろんマルチトラックの機能もあるので,それらを利用して本格的なDAWにしていきたいです.

参考文献

*1 https://atsushieno.hatenablog.com/entry/2020/04/19/055154

Discussion