Max/MSPでPythonを使う

Aug 11, 2020 23:24 · 1802 words · 4 minute read

Max/MSPでは従来のグラフィカルなプログラミング以外にもGenやJavaScriptなどのテキストベースのプログラミングもサポートされている. CやJavaでexternalを開発することもできるが,小さなアイデア毎にコンパイラを走らせるのはかなりめんどくさい. 今回はJavaScriptではなく,使い慣れているpythonで処理を記述できるか試してみた.

イメージとしては,pyというMax/MSPオブジェクトにスクリプトを渡せばその処理がオブジェクトに埋め込まれ,かつその処理内容は動的に変更できる,こんな感じである. 色々な方法があると思うが,まず思いついたのがpythonインタプリタが埋め込まれたC externalを作るということである.

これは普通に出来そうである. Boost.Pythonやpybind11といったライブラリにはpythonインタプリタをC++プログラムに埋め込む機能がある.

まずは普通にC externalを作る. ここでモダンなC++でC externalを開発できる Min-API というものを公式が出していることを知った.こんなの数年前にはなかった気がする.

C++14あたりが使えるのは嬉しいので,このMin-APIを使ってC externalを開発する. exampleを見るとこんな感じ.

/// @file
///	@ingroup 	minexamples
///	@copyright	Copyright 2018 The Min-DevKit Authors. All rights reserved.
///	@license	Use of this source code is governed by the MIT License found in the License.md file.

#include "c74_min.h"

using namespace c74::min;


class hello_world : public object<hello_world> {
public:
    MIN_DESCRIPTION	{"Post to the Max Console."};
    MIN_TAGS		{"utilities"};
    MIN_AUTHOR		{"Cycling '74"};
    MIN_RELATED		{"print, jit.print, dict.print"};

    inlet<>  input	{ this, "(bang) post greeting to the max console" };
    outlet<> output	{ this, "(anything) output the message which is posted to the max console" };


    // define an optional argument for setting the message
    argument<symbol> greeting_arg { this, "greeting", "Initial value for the greeting attribute.",
        MIN_ARGUMENT_FUNCTION {
            greeting = arg;
        }
    };


    // the actual attribute for the message
    attribute<symbol> greeting { this, "greeting", "hello world",
        description {
            "Greeting to be posted. "
            "The greeting will be posted to the Max console when a bang is received."
        }
    };


    // respond to the bang message to do something
    message<> bang { this, "bang", "Post the greeting.",
        MIN_FUNCTION {
            symbol the_greeting = greeting;    // fetch the symbol itself from the attribute named greeting

            cout << the_greeting << endl;    // post to the max console
            output.send(the_greeting);       // send out our outlet
            return {};
        }
    };


    // post to max window == but only when the class is loaded the first time
    message<> maxclass_setup { this, "maxclass_setup",
        MIN_FUNCTION {
            cout << "hello world" << endl;
            return {};
        }
    };

};


MIN_EXTERNAL(hello_world);

MIN_FUNCTIONとかいうマクロがうざいが(実際はただのラムダ),比較的すっきり書けそうである. これをベースにpythonインタプリタをpybind11を使って埋め込む.

C++における静的な型とpythonにおける動的な型をどうやり取りするかが一番問題となりそうだが,pybind11にはC++における型を吸収するpy::objectというクラスがあり,柔軟に対応してくれる.すごいぞpybind11!

流れとしては

  • C++の型はpy::objectにキャストしてpython関数にぶち込む.
  • python関数から戻ってきたpy::objectはC++の型にキャストする

ここで後者ではpython関数の戻り値の型をC++に教えてあげないといけなそうである. そこでMax/MSPオブジェクトに渡されたpythonスクリプトから関数のシグネチャをゲットしておくことで, どの型にキャストすればいいのかC++側で分かるようにした.

import inspect
import importlib
import typing


def signature(module_name, function_name):
    module = importlib.import_module(module_name)
    function = getattr(module, function_name)
    function_signature = inspect.signature(function)
    assert all(parameter.annotation is not parameter.empty for parameter in function_signature.parameters.values())
    assert function_signature.return_annotation is not function_signature.empty
    input_annotations = [str(parameter.annotation).split("'")[1] for parameter in function_signature.parameters.values()]
    output_annotations = [str(annotation).split("'")[1] for annotation in function_signature.return_annotation.__args__]
    return input_annotations, output_annotations

こんな感じだろうか.

import typing

def add(x: float, y: float) -> typing.Tuple[float]:
    return (x + y,)

こんな感じのpython関数があった場合に

>>> import signature
>>> signature.signature("add", "add")
(['float', 'float'], ['float'])

こんな感じで入出力の型情報をゲットできる. この関数をC++から実行すれば良さそうである. 便宜上,値を1つしか返さない関数でもタプルを返すことにする.

最終的に以下のようなコードをコンパイルするとpythonコードを実行するC externalが生成される.

#include "c74_min.h"
#include <pybind11/embed.h>
#include <pybind11/stl.h>
#include <boost/filesystem.hpp>

namespace py = pybind11;

class python : public c74::min::object<python> {

public:
    MIN_DESCRIPTION{"Embedded python interpreter"};
    MIN_TAGS{"utilities"};
    MIN_AUTHOR{"Hiroki Sakuma"};
    MIN_RELATED{"js"};

    python(const std::vector<c74::min::atom>& atoms = {}) {
        if (atoms.size() == 1)
        {
            const auto current_dirname = "/Users/hirokisakuma/Documents/Max 8/Packages/min-devkit/source/projects/min.python";
            py::module::import("sys").attr("path").cast<py::list>().append(current_dirname);

            auto module_name = static_cast<std::string>(atoms.front());
            m_function = py::module::import(module_name.c_str()).attr(module_name.c_str());

            auto signature = py::module::import("signature").attr("signature");
            std::tie(m_input_annotations, m_output_annotations) = signature(module_name, module_name).cast<std::tuple<std::vector<std::string>, std::vector<std::string>>>();
            
            for (const auto& input_annotation : m_input_annotations) {
                m_inlets.emplace_back(std::make_unique<c74::min::inlet<>>(this, "", input_annotation));
                m_inputs.append(0);
            }
            for (const auto& output_annotation : m_output_annotations) {
                m_outlets.emplace_back(std::make_unique<c74::min::outlet<>>(this, "", output_annotation));
            }
        }
        else {
            c74::min::error("only a script name required");
        }
    }

    c74::min::message<> number {this, "number", 
        [this](const std::vector<c74::min::atom>& atoms, const int inlet) -> std::vector<c74::min::atom> {
            assert(atoms.size() == 1);
            if (m_input_annotations[inlet] == "int") {
                m_inputs[inlet] = static_cast<int>(atoms.front());
            } else if (m_input_annotations[inlet] == "float") {
                m_inputs[inlet] = static_cast<float>(atoms.front());
            } else {
                c74::min::error("invalid input annotation");
            }
            m_outputs = m_function(*m_inputs).cast<py::tuple>();
            for (auto outlet = 0; outlet < m_outlets.size(); ++outlet) {
                if (m_output_annotations[outlet] == "int") {
                    m_outlets[outlet]->send(m_outputs[outlet].cast<int>());
                } else if (m_output_annotations[outlet] == "float") {
                    m_outlets[outlet]->send(m_outputs[outlet].cast<float>());
                } else {
                    c74::min::error("invalid output annotation");
                }
            }
            return {};
        }
    };
protected:

    std::vector<std::unique_ptr<c74::min::inlet<>>> m_inlets;
    std::vector<std::unique_ptr<c74::min::outlet<>>> m_outlets;

    std::vector<std::string> m_input_annotations;
    std::vector<std::string> m_output_annotations;

    py::scoped_interpreter m_interpreter;
    py::object m_function;

    py::list m_inputs;
    py::tuple m_outputs;
};

MIN_EXTERNAL(python);

boost::filesystemでソースコードのあるディレクトリをゲットしようとしたが,うまく出来なかったのでとりあえずハードコーディングしている…

add関数をMaxで使うとこんな感じ. 実際はここに与えられたシグネチャを満たす任意のpython関数をぶち込める. まだまだ洗練されていない感がすごいが,使っていく中で改良していきたい.