Tracktion Engineことはじめ ~DAWを作ろう~
この記事は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を使って,簡単なオーディオプレイヤーを作成しました.
オーディオファイルを読み込んで再生するだけのシンプルなアプリケーションです.
プロジェクト構成
src/
│
├── Main.cpp // アプリケーションのエントリーポイント
├── MainComponent.h // メインコンポーネントのGUIとロジック
├── AudioTrackComponent.h // オーディオトラックのGUI管理
├── Thumbnail.h // 波形表示クラス
└── Utils.h // ユーティリティ関数
Thumbnail.h
とUtils.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