よもやもダンプ

気が向いた時にアウトプットしておくところ

YM2608のトラッカーを作りました (BambooTracker)

GitHubBambooTrackerというYM2608向けのトラッカーを公開しています。
初リリースが2年前なので、ものすごく今更感のあるタイトルですが、やっと開発がひと段落しそうで、また他にブログのネタもないので、一旦トラッカー製作について簡単な記録をまとめてみようと思います。

github.com

簡単な紹介

BambooTrackerはFM音源チップYM2608(OPNA)を対象としたミュージックトラッカーです。

そもそもトラッカーとはミュージックシーケンサ(作曲ツール)の一種で、演奏データを縦方向に時系列順に並べて作曲していくシーケンサです。チップチューンで言うとFamiTrackerLittle Sound Dj (LSDj)などがトラッカー形式のツールです。
YM2608はかつてNEC PC-8801/9801シリーズに搭載されていた音源チップで、80年代90年代のコンピュータゲームにおいて多く使われた音源でした。

BambooTrackerはマウスによる編集が可能でWindowsMacLinuxに対応した、そして(たぶん)最初のYM2608用トラッカーです。
使い方・チュートリアルに関しては、maakさんが書かれた記事がおすすめです。

maakmusic.hatenablog.com

maakmusic.hatenablog.com

開発の動機

シンプルにYM2608を扱うトラッカーが無かったからです。

もともと自分はPMDでYM2608の作曲をしていました。MMLでの打ち込みも、FM音源でのチップチューンはそういうものだと考えていたので特に気にしていませんでした。
しかしチップチューンにのめり込むなかでトラッカーに触れ、ピアノロールとはまた異なるグラフィックベースの作曲手法が気に入りました。
また海外ではトラッカーがチップチューンにおいてメジャーなツールであり、YM2608のトラッカーが存在せずそれに対する需要がいくつか存在することに気づきました。
YM2608のトラッカーが存在しなかったのはこのチップがほぼ国内でしか流通しなかったので(嘘だったらゴメンナサイ)、海外では比較的マイナーな音源だったこと、ヨーロッパでのトラッカー全盛期にその文化を日本で接する機会が少なく、トラッカーに関する日本語の資料が少ないことが原因だと考えています。

またこれは個人の見解で偏見になるかもしれませんが、日本のFM音源チップチューンシーン、特にYM2608を扱う物は良くも悪くも閉じているように感じ、外からの新規層が入りにくい感覚がありました。その障壁の1つがMMLのみという作曲手段の少なさならば、トラッカーという手段が加わることで少しは緩和され、別の音源のトラッカーによる作曲のスタイル・手法をYM2608の作曲に持ち込み、新しいスタイルが生まれシーンも変わるのではないかという期待がありました。

こういった動機で海外向けにトラッカーという形式で開発を始めました。

BambooTrackerの名前の由来はBamboo=竹(take)で"Take (a) tracker (in your hands)"、トラッカー使ってみぃや(関西弁)ってことです。 ただ英語は正直苦手で英文のニュアンスや意味が違ってたら恥ずかしいので教えてください。

影響を受けたツール

主に0CC-FamiTrackerDeflemask TrackerGoatTracker 2を参考にしました。

開発を始めた時点で自分はそれほどトラッカーでの作曲経験がなかったので、いくつか調べて実際に触った中で個人的にGUI操作が優れていると感じたトラッカーを参考にしました。
クラシカルなトラッカーでは基本操作がキーボードで完結するような作りになっていることが多いですが、マウス操作がメインである現代ではそれに対応した方が初心者も扱いやすいでしょう。
特に0CC-FamiTrackerはFamiTrackerの派生であり、その操作方法も広く知られているものなので、BambooTrackerでは意図的に外観・操作性を近づけています。
ツールの操作を学ぶ学習コストを省いて作曲に集中できるようにすることを重視しました。

GoatTrackerはクラシカルなトラッカーに分類されますが、自分が最も使い慣れたトラッカーだったので、トラッカーの基本処理の実装法や機能の名称などはこのトラッカーの影響があります。

ただ開発しながら他のトラッカーを触って初めて知った機能などがあるので、もっと広く様々なトラッカーを使っていたならばより使いやすいトラッカーが作れたと思います。

基本構造

BambooTrackerは"マルチプラットフォームオープンソースのYM2608向けのトラッカー"として開発を行いました。
以下BambooTrackerの構造及び手法について述べていきますが、プログラミング経験が浅く、音声ストリーミング処理もほとんど経験がない中で色々試行錯誤したのでかなりゴチャついています。

開発言語はC/C++です。 Javaでもマルチプラットフォームなのは変わりませんが、事前にユーザがJREを入れないといけないのが個人的には手間がかかるので、ネイティブなコードを吐く言語を選びました。 後で調べてみるとJavaScriptなどによるWebベースのもの(BassoonTrackerなど)もあったので、自分が書きなれていたらこちらを選んだかもしれません。

BambooTrackerは実際の処理を司るコアクラス(class BambooTracker)、音声スレッドの管理クラス(class AudioStream)をQtライブラリを用いた各GUIクラスで共有する構造となっています。
BambooTrackerでの操作は、GUIイベントの中でコアクラスのメソッドを呼び出して内部データの処理を行い、外観に関する処理はGUIクラスで行うようコードを分離しています。 多少実装が複雑になりますが、これによって将来的にGUIライブラリを変更した場合でも、内部処理のコードを変更せずに外観のコードを新たに実装するだけなので移植が楽になります(と思います)。 ただ今後GUIライブラリを変えることは余程問題が起こる限りないと思うので、今となっては別に分離しなくてもよかったのではと思っています...

GUIライブラリは多く種類がありますが、マルチプラットフォームなライブラリの中からQtを選びました。 最初はSDLも考えていたのですが、ボタン類も自作しないといけないのが辛くて止めました。 wxWidgetsと悩んだのですが、Qtを使ったトラッカー(tutka)があったのでこちらを選びました。

コアクラスと音声ストリーミングクラスはコールバックで情報をやり取りします。 音声処理はもともとQtの音声関連クラスを使っていたのですが、なんとQtはGUIイベントと音声イベントが1つのイベントループで扱われるようで、描画処理と音声処理を捌ききれずにフリーズする事態が起こりました。 それぞれの処理をスレッドで分離するため、開発途中にRtAudioを用いて処理を行うよう変更しました*1

モジュールデータ

BambooTrackerでは曲データを大きい順に、モジュール-ソング-トラック-オーダー-パターン-ステップの単位で構成しています。基本的に上位の単位の中で複数の下位の単位を保持しています。
モジュールクラスはモジュールのタイトルなどのメタ情報、複数のソング以外にグルーブ情報も保持しています。 また、トラック-オーダー-パターンの関係は少し特殊で、トラッククラスは内部でオーダー列と複数のパターンクラスのインスタンス別々に保持します。トラッククラスはオーダー番号が与えられた時に、オーダー列から該当するオーダーに登録されているパターン番号を読み出し、適切なパターンを返す動作をします。

インストゥルメントの管理

インストゥルメントやそのプロパティはコアクラスで保持されているインストゥルメント管理クラスclass InstrumentsManagerで管理しています。 プロパティ(class AbstractInstrumentPropertyを継承したクラス)のインスタンスはインストゥルメントクラス(class InstrumentFMなど)に直接構築されず、管理クラス内でそれぞれを分けて保持しています。またインストゥルメントクラスはプロパティ番号と管理クラスの参照を保持させています。 これによって複数のインストゥルメントで同一のインストゥルメントプロパティを使用することが可能になります。
プロパティの編集や読み出しは管理クラスからプロパティにアクセスして行います。またインストゥルメントクラスからプロパティのデータを読み出す際には、内部で保持している管理クラスの参照を通してプロパティにアクセスしデータを読み出します。
また曲の演奏におけるティック(カウント)単位での適切なインストゥルメントプロパティの読み出しを行うためにプロパティのイテレータ(class SequenceIteratorInterfaceの継承クラス)を実装しています。

音源エミュレータ

自分がYM2608のエミュレータをいくつか触って感じたことを纏めてみました。

  • fmgen
    • MAMEのYM2608部のコードを参考にしたもの*2
    • クラス構造+ドキュメントが書かれているので使いやすい
    • SSG-EGの再現がうまくいっていない?
    • SSGの振幅は+最大値から-最大値
  • VGMPlay
    • 少し前のMAMEからの借用
    • SSGの振幅は+最大値から0
    • 合成周波数が高すぎる(2倍)
    • SSG-EGの再現がうまくいっていない
  • Nuked OPN-Mod

もともとfmgenとVGMPlayのエミュレータを比較してSSGの再現度からVGMPlayを取りましたが、後程上に挙げた問題が発覚しました。合成周波数はコードを修正しましたが、SSG-EGは直せていません。どうも本家MAMEのコードも間違っているようです*5
Nuked OPN-ModはSSG-EGの再現性の問題が発覚した際に追加しました。現時点では恐らく最も精度の高いエミュレータだと思っていますが、SSGやADPCMに関しては微妙に違いがあるので、今後さらに高精度のエミュレータが出ることを期待しています。

YM2608はFM&ADPCMがデジタル出力、SSGがアナログ出力のため外部でミキシングを行っている関係でサウンドボードによってこれらの音量バランスが異なります。 詳しい音量比についてはドキュメントが見当たらず、fmgenを使用しているhootのドライバ設定が最も近いと判断し、hootの出力からおおよその音量比を算出しました*6
こういった経緯により、BambooTrackerでは曲のプロパティで各ボードに対応した音量比を選択できますが、その正確さは保証できません。将来的には実際のボードと比較して修正を行いたいです。

音源エミュレータの制御

音源エミュレータの制御はnamespace chip内のクラス及びclass OPNAControllerで行います。

音源エミュレータレジスタ書き込み処理のコードを変更せず、複数のエミュレータを切り替えやすくするため、ラッパークラスchip::OPNAの内部で保持する形をとっています。 エミュレータの細かな設定などはラッパークラスの内部で行うようにし、レジスタの読み書きなどのトラッカーにとって必要な処理のみをラッパークラスのメソッドとして用意しています。 またラッパークラスはリサンプラ―chip::LinearResamplerを内部で保持しています。

音源エミュレータ(VGMPlayとNuked OPN-Mod)では、FMとSSGはそれぞれ生成されるサンプルのレートがFMとADPCMが55466Hz、SSG249600Hzがとなっています。 そのためラッパークラスのサンプル生成メソッド(void chip::OPNA::mix(...))で、この2つをvoid LinearResampler::interpolate(...)を通して任意のレートに線形補間によるリサンプリングを行います。 また、このメソッドでは同時に先述した音量バランスの調整も行ったうえでサンプルのミキシングを行います。
リサンプリング手法については実装が簡単で処理が高速な線形補間を用いましたが、この手法は場合によっては音が大きく歪んでしまうため、演算速度に問題がなければSinc補間などの手法に変えた方がいいです。

ラッパークラスでもレジスタ書き換えが行えますが、キーオン、キーオフ、インストゥルメントや音量、パターンエフェクトの設定などのイベントに対応したメソッドを持つ音源制御クラスclass OPNAControllerを定義し、それぞれのイベントの処理を扱いやすくしています。 設定されているエフェクトやインストゥルメントプロパティのシーケンスの実行位置の更新も、このクラスで行います(void OPNAController::tickEvent(...))。 また音量マスクなどもこのクラスで記憶しているため、演奏において非常に重要な役割を果たします。

演奏制御

コアクラス中の演奏制御クラスclass PlaybackManagerで曲の演奏を制御します。
このクラスでは先程の音源制御クラス、インストゥルメント管理クラス、モジュールデータクラスのそれぞれの参照のほか、演奏カウント計算クラスclass TickCounterを保持しています。

演奏カウント計算クラスでは、現在のテンポとステップサイズ、またはグルーブの設定から適切な現在のステップサイズ(=1ステップ辺りのカウント数)を算出し、何カウント後に次のステップに遷移するかを求めます。
BambooTrackerで設定できるステップサイズ(UIではSpeedと表記)はあくまで目安の値です。実際は演奏速度をテンポに一致させるため、このクラスで1ステップのカウント数を適宜変動させています。

演奏制御クラスでは演奏開始、停止のほか、ティック更新に伴い内部の演奏カウント計算クラスや現在の再生モード(ループ再生など)をもとに演奏位置を設定し、必要であればパターンデータを読み出し音源制御クラスのイベントメソッドを実行させます。 ディレイ系のパターンエフェクトにおける実行待ち処理はこのクラスで行います。
パターンエフェクトはステップの最初のティックで実行するもの、ディレイ系、ステップの最後のティックで実行するもの(演奏終了など)の3種類に分けられ、それぞれ実行するタイミングが異なります。 このことを踏まえて演奏制御クラスはステップの最初のティックの実行時に行うステップデータ読み込み処理を以下の順に行います。

  1. 全トラックのステップに登録されているエフェクトを読み込み、実行待ちエフェクトマップに登録する。
  2. 各トラックごとにディレイエフェクトが発生しているか確認する。
  3. ディレイエフェクトが発生していない場合は音量、インストゥルメント、エフェクト、キーオン/オフの順に音源制御クラスにデータを設定する。ここでエフェクトは最初のティックで実行するエフェクト用の実行待ちエフェクトマップに登録されているものを取り出す。
  4. 2,3を全トラックで実行する。

ディレイ系や最終ティックで実行するエフェクトは、実行タイミング到達時にそれぞれの実行待ちエフェクトマップからデータを取り出し音源制御クラスへ送ります。

ストリーミング

ストリーミングは基本構造の項目でも述べた通り、コアクラスと音声ストリーミングクラスの間で情報をやり取りしながら行います。
ストリーミングクラスのRtAudioのコールバック処理では、音源エミュレータからサンプルを生成し、引数で与えられた配列に格納していきます。 サンプルの生成する区間中にティックのカウントが変更されるポイントがある場合はその地点で一度生成をストップし、コアクラスでそのティックでの演奏イベント実行して再びサンプルの生成を行います。

このストリーミング手法の問題点として、サンプル生成時に演奏カーソル位置の移動を行っているため、生成されるサンプル数によっては1度に複数ステップのカーソル移動が起こり、演奏時のトラッカーのパターンを上から下へスクロールする動きが飛び飛びとなってぎこちなくなる可能性があります。

ユーザインターフェース

インターフェース及び大方の機能は0CC-FamiTrackerと同様の操作が行えるように実装しました。 ただ0CC-FTのコードはあまり確認していないので、内部の実装方法はかなり違っているかもしれません。
オーダーリストやパターンエディタ、インストゥルメントエディターのシーケンスエディターなどはQtで用意されているはずがないので、class QWidgetを継承したクラスで作りました。

多くのトラッカーは現在のステップ位置が中央になるようパターンエディターを描画しています。よって再生時にはパターンが下から上へスクロールするような動作をします。
スクロール可能なウィジェットの描画にはclass QScrollAreaclass QGraphicsViewを使う手もあったのですが、これらではスクロールがピクセル単位で表示位置を移動させます。 しかしBambooTrackerではオーダーまたはステップ単位で表示位置を移動させるため、class QWidgetclass QScrollBarを別々に配置し、スクロールイベントに関してそれぞれを接続させて、カーソル位置の変更のたびオーダーリストやパターンエディターを再描画する手法を取りました。

オーダーリストやパターンエディターは、オーダーやステップ移動が再生時に頻繁に行われるため、再描画の処理は出来るだけ高速に行う必要があります。
Qtではウィジェットの外見の更新をupdate()によって呼び出すことが多いですが、これはQtのイベントループのキューに描画イベントをエンキューするので、再描画の実行までに時間がかかることがあります。
BambooTrackerではupdate()ではなくrepaint()を呼び出しています。これはupdate()とは違い呼び出しの時点で即座に再描画が実行されます。 repaint()の使用時の注意として、update()では実行前に複数回呼び出したり、描画処理の中でupdate()を呼び出しした場合でも実際の実行回数は1度だけになりますが、repaint()ではそれらは考慮されないのでフラグなどを用いて再帰や複数回の呼び出しを防ぐ必要があります。

また描画処理も前回の一部の描画データが使える場合は再利用することで再描画処理の省略を行っています。
パターンエディタの場合ではclass QPixmapを描画レイヤーとして、外観を4つのレイヤー(背景、テキスト、前景、ヘッダ)の重なりで表現します。 パターンに音符を入力した場合、見かけでは音符のテキストが追加されただけの違いなので、テキストレイヤーのみ更新するだけで処理は済みます。
またカーソルの上下移動による再描画では、ヘッダ以外のレイヤーを必要分だけシフトして一部の再描画を行うことで、全体を一から書き直す場合よりも処理数を削減することができます。

その他の機能

他のFM音源系のトラッカーやMMLになさそうな機能をいくつか実装しました。

音色データのコピー&ペースト

FMインストゥルメントエディターでの音色パラメータや各オペレータの値を右クリックのメニューからコピーできるようにしました。 データはテキストデータとしてクリップボードに保存され、他のインストゥルメントやオペレータに貼り付けできるようになっています。

また、MMLから音色定義のテキストをコピーしてエディタ内で特殊貼り付けすることで音色を読み込むことができます。

これは環境設定の中にあるFM envelope textの設定で、コピーしたテキストに含まれる数値を出現順に設定したFM音色パラメータに割り当てることで実現しています。 コピーしたテキストからの数値の読み出しは正規表現を用いて抜き出しています。

FMエンベロープリセット

FMではキーオン時に前回のエンベロープ状態を記憶したまま新たなエンベロープを実行するので、アタックがゆっくりな音色を消音する前にキーオンするとアタック速度が短くなることがあります。 BambooTrakcerではFMインストゥルメントエディターで次のキーオンの1ティック前にエンベロープを強制的に0にするFMエンベロープリセット機能を有効にすることができます。 先述した通りエンベロープはキーオフしただけでは0にならないので、同時にリリースも31に設定して即座に消音するようにします。

次のキーオンの数ティック前で実行するといえば、MMLではゲートタイム(PMDだとqコマンド)が存在します。MMLではゲートタイムのコマンドは上述のエンベロープリセットのほかに音の区切りをはっきりさせたい時などによく使われますが、BambooTrackerではこの機能を実装していません。
MMLでは音符単位で演奏情報を記述し、しかもコンパイルを行うので単に長さの計算をすればいいのですが、トラッカーでは音長の概念がなく、次のキーオンの位置を検索した後に実行する位置をリアルタイムで計算する必要があります。 BambooTrackerは先述した通り、ステップの実際のカウント数は演奏中に計算されるため、事前にステップの実際のカウント数を計算することが困難です。 FMエンベロープリセットでは1ティック前に実行するので必ずキーオンする1つ前のステップの中に実行位置が存在するため実装が可能でしたが、ゲートタイムではステップの大きさを超えるティック数での動作も求められるため実行位置の算出がより複雑になります。
ゲートタイムは自分もMMLでは必ず使うコマンドの1つなので、ほとんどのトラッカーに実装されていないのが少し不便に感じることがあります。上記の理由でBambooTrackerでは泣く泣く実装を断念しましたが、逆にステップサイズが固定のトラッカーならば実行位置の計算も簡単になるため実装可能だと思います。

SSG音源の表現の強化

SSG音源は基本的に矩形波とノイズを鳴らし、ソフトウェアエンベロープで音量を制御するものが多いですが、BambooTrackerではZX SpectrumやAtari STなどのチップチューンで用いられる手法を使えるようにしました。 手法についての詳しい内容は過去の記事を参照してください。

rerrahkr.hatenablog.com

現時点ではBuzzer effectが実装できています。

出来ればそれ以外のテクニック、特にSID voiceも取り組みたいと思っていますが、今のところBambooTrackerが実機向けにエクスポートするVGMやS98ではこれらを再現することは不可能なので、今後どうするかは未定です。

ADPCMサンプルエディター

ADPCMのサンプルを単に鳴らすだけでも十分ですが、サンプルのループ機能と合わせてMODトラッカーのサンプル編集のようなことができるエディターを実装しました。

マウスで波形を描画して、波形メモリ音源のような使い方ができます。ただし、波形メモリ音源とは異なりADPCMは変化の制約があるので編集の自由度はMODトラッカーのそれと比べると劣ります。 またBambooTrackerではADPCMのループする単位は64サンプルなので、再生レートと基準音程を上手くやりくりしないと音色としては使い辛いです。玄人機能ですね。

ピッチテーブル

FMやSSGの音高はレジスタに書き込まれた数値から計算されますが、レジスタ値を1ずつ変更していく時、音高は直線的な変化ではなく微妙にカーブしたものになります*7。 この場合ビブラートなど一定値の範囲でピッチを変化させるエフェクトを実行すると、基準音の高さによって実際の音高の変化も変わってしまいます。
BambooTrackerでは予め半音間を32段階で直線的に変化するように計算したピッチテーブルを保持しておき、適宜参照することでどの音高でピッチ変化を行っても変化の度合いが同じになるようにしています。


あとがき

もともとはトラッカーとMMLの融合を目指していたので、両方のいいところを混ぜ合わせたものを作りたかったのですが、実際にできたのは妥協の産物でした。 他のソフトとの互換性(特にPMDとの)を意識すると、逆に自由な編集ができなくなる部分もあるので、そのあたりどこで折り合いをつけるかが難しかったです。
また最初はPC-98の実機を持っている人は限られているので、エクスポートしたものが実機で鳴ればそれでいいというスタンスだったのですが、海外でも実機の環境を想定して作曲したい人が思った以上にいたので、途中で実機での挙動を考えて何の機能を実装するか考えるようスタンスを変えて、全体的に中途半端な作りになってしまっている感じがします。 実機にこだわる理由として、単純に「本物」で鳴らしたいというのもありますが、過去の資産、特にFMDSP、PMDSPなどのプレーヤーで鳴らしたいというというのがあって、自分にとっては盲点でした。確かにFMDSPとかは見た目がカッコいいですからね。 余談ですが、BambooTrackerのデフォルトのオーダーリスト、パターンエディターの色が紫系統なのはこれらのプレーヤーの影響です。

BambooTrackerはトラッカーの知識も資料もあまりなく、手探りの状態から作ったので今振り返ると色々こうすればよかったという点が多いです。 ここに書いてる物も恐らくもっと良い実装方法があります。
今後の開発は今のところ何も考えていませんが、もしかするとここに書き連ねたトラッカーの構造や手法を修正するかもしれません。


技術系の記事を書きなれていないので綺麗な記事ではないと感じていますが、日本語でのトラッカー製作に関する記事として色々忘れる前に書き残しておきます。