こんにちは!こんにちは!
FOLIO Advent Calendar 2025 6日目を担当する石川です。
厳しい寒さが続き、冬の陽だまりがことのほか暖かく感じる歳末の候、皆さまいかがお過ごしでしょうか。
これは何
Max for Liveでオリジナルの制作ワークフローを構築する楽しさを感じるここ数ヶ月です。
開発をする上で作ったユーティリティクラスを2つ紹介しています。
なんとなく対象読者
上記に該当せずとも、興味関心を持っていただけると幸いです。
よって「Ableton Live」と「Max for Live」を知らない方もいらっしゃると思いますので、簡単な説明も挟んでいます。
なお、パッチングやオブジェクトの細かい説明は割愛させていただきます。
端的に言えば音楽制作ソフトウェアです。
世の中にはCubase、Studio One、Logic Pro...といった選択肢もあるワケですが...
「Live」は名は体を表すようにライブ演奏やリアルタイム操作に強みがあります。
「Max for Live(M4L)」とは何か
Live内の機能を自作のプログラムで拡張できる仕組み、と言ったら言うべきか...。
専用のヴィジュアルプログラミング環境(Max)を用いて、エフェクターやオートメーションツールなどを開発できます。
他の開発者が配布しているプログラム(デバイス)も自身の環境にインストールして使用することもできます。
開発スタイル
基本的にはヴィジュアルプログラミングのような形式で開発を進めていきます。
画面上で「オブジェクト」と呼ばれる部品を配置・配線する形になります。
これを「パッチング」と呼ばれていたり、呼ばれていなかったりします。(曖昧)
Maxの独特のクセに このパッチングに慣れてしまえば多くの処理は視覚的かつ直感的に組めますし、コードを書くよりも手っ取り早く済む面もあるでしょう。
が、ありがたいことにJavaScript(V8エンジン)も搭載されているようです。
これによってテキストベースのプログラミングに馴染み深い私の土俵に引っ張り出すことができるワケです。
パッチ上のダイアルやメニューの値をJavaScript側で受け取り、Liveに命令を送るといったことが可能です。
パッチで完結するケースと、JavaScriptで書いたほうがラクできるケースを適宜使い分けていきましょう。
このあたりの棲み分けというか、設計方針に関しても論ずる点が割とあるのですが話が長くなりそうなので省略します。
live.thisdevice について(補足)
live.thisdevice reports three pieces of information about your Max Device. A bang message is automatically sent from the left outlet when the Max Device is opened and completely initialized, or when the containing patcher is part of another file that is opened. Additionally, a bang will be reported every time a new preset is loaded or the device is saved (and thus reloaded within the Live application). A 1 or 0 will be sent from the middle outlet when the Device is enabled or disabled, respectively. A 1 or 0 will be sent from the right outlet when preview mode for the Device is enabled or disabled, respectively. Used within Max, live.thisdevice functions essentially like the loadbang object. The middle and right outlets are inactive in this case.
https://docs.cycling74.com/reference/live.thisdevice より引用。
ひとまずこのようにパッチングをしておきましょう。
ちなみに左側はパッチング画面、右側はコンソール(ログ)画面です。
main.js の内容は以下のとおりです。
function bang() {
post("Device initialization complete! \n");
}
このオブジェクトを使用することでデバイスが確実に初期化(読み込まれた)ことを担保することができます。
完全に初期化されていない状態で、LiveAPIやその他の処理を行うとエラーが起きがちです。
live.thisdevice からの通知を受け取ってから処理を実行することがセオリーと思います。
post関数
上述のソースコードを読んでみると不思議な post 関数が出てきました。
Prints a representation of the arguments in the Max window.
If post() has no arguments, it prints starting on the next line. Otherwise it prints the input on the current line separated by spaces. Arrays are unrolled to one level as with jsthis.outlet().
post - Max JS API | Cycling '74 Documentation より引用。
JavaScriptでは見慣れた console.log() を使いたくなるところですが...Maxでは機能しません!
Maxでは post 関数を使うことでコンソールへ文字列を出力することができます。
出力する毎に改行を挟みたいので、なんとなくさっくりと以下の関数を用意してしまうのも手でしょう。
function lnPost(obj) {
if (obj) {
post(obj + "\n");
}
}
ロガー
...という話を踏まえた上で今回は人間に優しい形でログを出力するユーティリティクラスを実装しました。
形としてはよく見かける実装ではないでしょうか。
ログの出力例
v8: 2025-11-29T14:56:26.144Z [INFO] main: Device initialization complete!
開発中のデバッグに何かと便利と思います。
JSON形式での出力やログレベルの設定といった味変はおまかせします。
「Live API」について
M4Lには、Liveの内部状態の取得および操作するためのクラスとして Live API が提供されています。
これによってトラックやエフェクターのパラメーターなどにアクセスし、プログラマブルに操作することが可能です。
LiveAPI - Max JS API | Cycling '74 Documentation
Liveの内部データ構造は階層構造となっており、これを「Live Object Model(LOM)」と定義されています。
Live Object Model | Cycling '74 Documentation より引用
プロジェクト全体(live_set)の中にトラック(tracks)があり、その子供としてデバイス(エフェクターなど)、デバイスのパラメーター...などとぶら下がっている具合です。
各オブジェクトにアクセスする際は、文字列でパスを指定するような形となっています。
たとえば、プロジェクトの任意のトラックにアクセスする場合は、live_set tracks 0のようなパス指定します。
(初めてドキュメントを見たときにAPIの作りとしてかなり違和感がありましたが一旦は飲み込むこととしました。)
const track = new LiveAPI("live_set tracks 0");
コンストラクタにはパス以外にもコールバックを設定することができます。
基本的には子オブジェクトに変化(トラックの追加やパラメーターの変更)を監視することを目的としています。
property string
The observed property, child or child-list of the object at the current path, if desired
For instance, if the LiveAPI object refers to "live_set tracks 1", setting the property to "mute" would cause changes to the "mute" property of the 2nd track to be reported to the callback function defined in the LiveAPI Constructor.
LiveAPI - Max JS API | Cycling '74 Documentation より引用
const liveSet = new LiveAPI(() => {
logger.info("callback invoked!");
}, "live_set");
liveSet.property = "tracks"
このコールバックは Live API がインスタンス化された場合と property に設定したトラック(tracks)に変化があった場合に呼び出されます。
「Live API」のコールバックの挙動が不思議だった
さて、先述したようにこのコールバックについては以下の通りであるわけですが...
a function to be called when the LiveAPI object refers to a new object in Live (if the LiveAPI object's path changes, for instance) or when an observed property changes
LiveAPI - Max JS API | Cycling '74 Documentation より引用
なんとなく、監視対象のオブジェクトに変化があった場合にのみ処理を行いたい気持ちが出てくるわけです。
順当に初期化済みフラグを立てて条件分岐で早期returnすると良さそうに思います。
const logger = new Logger("init");
let initialized = false;
const liveApi = new LiveAPI(() => {
logger.info("LiveAPI callback invoked");
if (!initialized) {
initialized = true;
logger.info("Initialization complete");
liveApi.property = "tracks";
logger.info("Setting property to tracks");
return;
}
logger.info("Detected change in live_set tracks");
}, "live_set tracks");
これでトラックが追加・削除といった変化があった場合にのみ、処理を継続することが期待できそうです。
が、実際に出力されたログは以下のとおりです。
v8: 2025-11-06T20:50:22.704Z [INFO] init: LiveAPI callback invoked
v8: 2025-11-06T20:50:22.704Z [INFO] init: Initialization complete <- 初期化フラグが立った(期待した動作)
v8: 2025-11-06T20:50:22.704Z [INFO] init: LiveAPI callback invoked
v8: 2025-11-06T20:50:22.704Z [INFO] init: Detected change in live_set tracks <- トラックの変更が行われていないもかかわらず、変更が検知された(期待しない動作)
v8: 2025-11-06T20:50:22.705Z [INFO] init: LiveAPI callback invoked
v8: 2025-11-06T20:50:22.705Z [INFO] init: Detected change in live_set tracks <- 再びトラックの変更が行われていないもかかわらず、変更が検知された(期待しない動作)
v8: 2025-11-06T20:50:22.705Z [INFO] init: Setting property to tracks <- 監視対象のオブジェクトを設定された
--- 手動でトラックを追加した場合にのみハンドラが呼び出されている ----
v8: 2025-11-06T20:50:37.048Z [INFO] init: LiveAPI callback invoked
v8: 2025-11-06T20:50:37.048Z [INFO] init: Detected change in live_set tracks
監視対象のオブジェクトの設定が設定される前に、トラックの操作をしていないにも関わらず変更が検知される不思議な挙動に遭遇しました。
プロパティが更新されたあともコールバックが呼ばれ、プロパティを設定した時点では initialized フラグが true となり変更が検知されたかのような挙動になっている気がします。
というわけで初期化フラグに加えて、プロパティ設定済みフラグも用意してみることにします。
const logger = new Logger("init");
let initialized = false;
let detectedPropertySet = false;
const liveApi = new LiveAPI(() => {
logger.info("LiveAPI callback invoked");
if (!initialized) {
initialized = true;
liveApi.property = "tracks";
logger.info("Initialization complete & Property set to tracks");
return;
}
if (!detectedPropertySet) {
detectedPropertySet = true;
logger.info("Detected property set to tracks");
return;
}
logger.info("Detected change in live_set tracks");
}, "live_set");
出力されたログは以下のとおりです。
v8: 2025-11-09T04:12:21.709Z [INFO] init: LiveAPI callback invoked
v8: 2025-11-09T04:12:21.709Z [INFO] init: LiveAPI callback invoked
v8: 2025-11-09T04:12:21.709Z [INFO] init: Detected property set to tracks
v8: 2025-11-09T04:12:21.709Z [INFO] init: LiveAPI callback invoked
v8: 2025-11-09T04:12:21.709Z [INFO] init: tracks changed
v8: 2025-11-09T04:12:21.709Z [INFO] init: Initialization complete & Property set to tracks
またしても、監視対象のオブジェクトを触っていないにもかかわらず変更が検知されているように見えます。
プロパティが設定されたあともコールバックが呼ばれるのでしょうか?
さらにフラグを足してみます。
const logger = new Logger("init");
let initialized = false;
let detectedPropertySet = false;
let detectedFirst = false;
const liveApi = new LiveAPI(() => {
logger.info("LiveAPI callback invoked");
if (!initialized) {
initialized = true;
liveApi.property = "tracks";
logger.info("Initialization complete & Property set to tracks");
return;
}
if (!detectedPropertySet) {
detectedPropertySet = true;
logger.info("Detected property set to tracks");
return;
}
if (!detectedFirst) {
detectedFirst = true;
logger.info("Detected first callback after property set");
return;
}
logger.info("Detected change in live_set tracks");
}, "live_set");
ログの出力は以下の通り
v8: 2025-11-09T04:22:47.803Z [INFO] init: LiveAPI callback invoked
v8: 2025-11-09T04:22:47.803Z [INFO] init: LiveAPI callback invoked
v8: 2025-11-09T04:22:47.803Z [INFO] init: Detected property set to tracks
v8: 2025-11-09T04:22:47.804Z [INFO] init: LiveAPI callback invoked
v8: 2025-11-09T04:22:47.804Z [INFO] init: Detected first callback after property set
v8: 2025-11-09T04:22:47.804Z [INFO] init: Initialization complete & Property set to tracks
--- 手動でトラックを追加した場合にのみハンドラが呼び出されている ----
v8: 2025-11-09T04:25:46.433Z [INFO] init: LiveAPI callback invoked
v8: 2025-11-09T04:25:46.433Z [INFO] init: Detected change in live_set tracks
それとなく当初の目的は達成できたような気がします。
これをうまくラップしたクラスを実装してみました。
ログ出力は以下の通り
v8: 2025-11-29T18:02:08.331Z [INFO] main: Device initialization complete!
v8: 2025-11-29T18:02:08.332Z [INFO] LiveObjectObserver: LiveAPI initialized
v8: 2025-11-29T18:02:08.332Z [INFO] LiveObjectObserver: Detected property set
v8: 2025-11-29T18:02:08.332Z [INFO] LiveObjectObserver: Detected first change notification
v8: 2025-11-29T18:02:08.332Z [INFO] LiveObjectObserver: Set property [property=tracks]
--- 手動でトラックを追加した場合にのみハンドラが呼び出されている ---
v8: 2025-11-29T18:02:13.937Z [INFO] main: Tracks changed
v8: 2025-11-29T18:02:15.321Z [INFO] main: Tracks changed
変更自体は検知できますが、状態の差分はLiveAPIでは提供されていません。
必要に応じて状態と差分を管理するロジックを実装すると良いでしょう。
しかし...このLiveAPIのコールバックの挙動に半月ほど悩まされました~...
おわりに
何かと想定と異なる挙動に何かと遭遇する場面が多々ありつつ、良くも悪くもなんとか慣れてきた今日この頃です。
こじんまりとしたプラクティスでしたが、同じ轍を踏まないことを祈り記事にした次第です。
それでは良いお年を。
追伸
株式会社FOLIOでは現在、メンバーを募集中です!
会社の雰囲気や仕事の様子など、カジュアル面談でぜひお話ししましょう。
カジュアル面談(社員紹介専用フォーム/社員紹介の方のみこちらからご応募下さい) - 株式会社FOLIO
追追伸
楽器の経験がある方や現在進行形で音楽活動をしている方など、音楽に親しみがあるメンバーが何気に多い会社に思います。
音楽性は違えど 互いを尊重し、各々の専門領域を活かしながら、妥協ない仕事ができる環境です。
会社の雰囲気や仕事の様子など、カジュアル面談でぜひお話ししましょう。(大事なことは2回言うタイプ)
カジュアル面談(社員紹介専用フォーム/社員紹介の方のみこちらからご応募下さい) - 株式会社FOLIO