音楽制作用ライブコーディングツールを製作中。(新バージョン)
以前こんな記事を書いてました。
当該ツールについてだいぶ作り直した結果、もはや別物になっている旨の記事です。
概要
現在ライブコーディングで作曲ができるツールを開発しています。
スクリプトを書いてコンパイルすると、譜面が生成されて音楽が再生されます。
今のところ開発2週間目ですが、色々機能が揃ってきてます。
こんなツールです。
アプリを起動し、VSCodeでテキストを編集し、編集したテキストをアプリ側に読み込ませることで音楽を演奏できるという内容です。
この記事は、似たようなツール作りたい人が参考にするかなという目的で忘れないうちに開発途中だけどまとめるものです。
開発経緯
今回は以下の理由で作りたいと思いました。
- 自分が作曲を趣味とするが、腱鞘炎でマウスが使えなくなったので、代替するツールで作曲できるようにする
- 自分がよく使うWindows環境でライブコーディングツールがほしい
- 以前作ったツールを改善したい(https://machiaworx.itch.io/recode-livecoding)
- SESSIONS開催
今回は4番目は大きな理由ですね!
せっかくなので開発して持って行く(+動画提出)予定です。
みんなもSESSIONS参加しようぜ!
仕様
この記事も「仕様を忘れないうちにまとめる」目的で記載しているので、仕様や実装について列挙していきます。
開発言語
C++で開発しています。(リソースの持ち方の関係でBetterCぽく開発してますが)
元々Unityで開発していたのですが、MIDI出力を正確に行える手段がこのときに存在しなかったのと、波形生成に伴うマルチティンバー出力に対応していると、デバイス絡みの負荷が半端なくなってきて、ついにはノイジーになってきたので限界を感じて取りやめた経緯があります
以前から開発再開したらこの技術を使ってこういう仕様で、というのは考えていましたが、ちょうど前述のイベント開催の知らせを聞き、この開発期間であれば間に合うのでは?と考え、開発することにしました。
C++は仕事ではまったく触りませんが、メガデモ製作で何度も使っていることもあって、それほどの苦労はありませんでした。(過去にソフトウェアシンセサイザーも自分で作ってます)
利用ライブラリ
以下のライブラリ利用中です。
- MiniScript
- miniaudio
- PortMidi/PortTime(付属ライブラリ)
- Dear Imgui
- GLFW3
意外とシンプルですね。
仕組み
- メインスレッドでGUI操作
- コールバック関数でminisoundが動作する(スレッド立てられてるみたいで、GUIに伴う停止もなし)
- データサーバとして各種リソースを集中管理する
- データサーバ内部でMiniScriptのスクリプトエンジンが動作する
- ボタン押下or定期タイミングでコンパイル→譜面生成までを行う
- miniaudio側で特定条件を満たした時に譜面を読み始めてコールバック上でレンダリングを行う
- ついでにコールバック上でMIDI出力も行う
サウンドのレンダリングエンジンを超改造して、「複数波形発音+MIDI出力」ができるようになりました。
本来なら複数波形発音はライブラリ使う状態だと所定の手続き行う必要があるのですが、これを回避し自前でシンセサイザーの波形をレンダリングする仕組みを追加+譜面読み込みの仕組みをコールバック上で参照できるため、ここにMIDI出力の機能も追加してます。
結果、MIDIと自前シンセがほぼ同期します。
ちょっとしたイニシャライズのズレはありますが、補正できそうなのでこれは修正予定。
オーディオ+MIDIコールバックの仕組み
まず、miniaudioが共通IFとともに、特定波形を再生するサンプルを提供しています。
これを改造し、波形サンプルを複数本流せる+各波形サンプルの音量を変更できるように実装しております。
更に譜面読み込みについてもオーディオサンプル単位で位置を計測し取得できるようにしたため、特定のタイミングで音程を変更することも可能になりました。
MIDIについてはPortMIDI上でスケジュール機能が存在するため、これと譜面データを利用し、譜面データ上トリガーになる部分とバッファ部分の間隔調整を行い再生できるようにしております。
※当初別スレッドでMIDIデータ制御を行う予定でしたが、時間間隔に相違が出始めたため、先に実装していたオーディオコールバックにMIDI制御を加えることを検討し始めました。
スクリプト言語の組み込み
前述の通り、MiniScriptという言語を使って譜面を保存しているのですが、元々C#で使うことを想定していた言語であり、C++でも作成されているのですが、仕様は同じでも実装がまったく違っており、組み込みがトリッキーになったので、内容を記載しておきます。
文字列・リスト等の利用クラスが独自のもの
List/Map/Stringクラスが独自のものとして用意されておりますが、これはSTLとの互換性はないため、既存のSTLクラスと混ぜて使うと混乱を招き、かつ形に不整合が起きるため、混ざらないように使う必要があります。
例:namespaceをあえて定義せず使う
スクリプト側変数の保持する場所に違いあり
C#では値渡しがメイン+クラス単位で移譲処理ができるため、長期間保持する変数をクラス内で持っていても問題ありません。
しかし、C++で作る場合、クラスに紐づく概念が存在しない(移譲機能が言語仕様として存在しない)ため、別の機能で保持する必要があります。
C++版では変数がスクリプトを実行するVM上で保存されており、Valueクラス(MiniScriptの変数を参照するクラス)はあくまでVM上にあるデータを参照するだけの機能を持っております。
よって、実行が終わった後、ローカル変数へのアクセスはできなくなります。
この事から、変数を保存する場合、実行が終わる前に変数を別の場所に格納する等の対応が必要になります。
ただ前述のとおりValueクラスは参照する機能しか持たないため、VM上にどうにかしてスクリプト実行後もデータを持たせておく必要があります。
これはVM上にグローバル変数を保存しておく領域「GlobalContext」があるため、ここからSetValueメソッドで保存することが可能です。
グローバル変数の保存方法がVMの仕様に準拠する必要あり
MiniScript側の変数が前述の通りGlobalContextに保存されているのですが、このアクセスについては「アクセスした時点で」値が空の場合Exeption扱いになります。
つまりアクセス前に変数等で管理しておいてアクセスしたかを監視する必要があります。
更に前述の通り同じVMを使い回す場合、GlobalContext上の変数が永続的に使える保証はないため、何らかの理由で別の変数に保存する必要があります。
ただ、前述のとおりValueクラスは参照しか行えません。これが数値や文字列だったらGetDouble()やGetString()と言ったメソッドがあるため型をC++が対応するよう変更して値渡しで保存という手はありますが、今回利用したかったのが、MiniScript内で利用する「List」という形式(機能はSTLのListと似てる)のため、MiniScriptのルールに準拠する必要があり、そのままの変数保存はできない形になりました。
結果どうしたかというと、「VMをもう一個用意し、毎回更新しない代わりにスクリプトへのアクセスもできないようにし、グローバル変数を保存する用途で確保する」ようにしました。
これによって、毎度スクリプトを実行しつつ、変数も確保しつつ、かつ永続的に保存した変数を使えるようになる、という仕様が作成できたことになります。
最後に
今後も仕様についてはまとまったら記載しようかと思いますが、まずはSESSIONS開催決まったね!ということでライブコーディングツール開発再開しているよ、という報告でした。
ツール自体は11月公開予定としていますが、今のところ利用に耐えうる機能はほぼ用意できてるため、公開時期を早めるかもしれません。
Discussion