Elixir と SuperCollider で音楽を奏でてみようという試みです。
きっと業務には全く生かせません。
前提
参考までに当方の環境は以下。
- OS: Mac OSX 10.11.1
- Elixir : v1.1.1
- SuperCollider : v3.6.6
また、この後説明していきますが、 Elixir と SuperCollider の使い分けはこんな感じ。
- 曲の制御 : Elixir
- 音の生成 : SuperCollider
このような構成のものは、 Elixir 以外の他の言語だと
といったものがあります。他にもいくつかあります。
Elixir でも似たようなものあればそれを使って進めようかと思ったんですが特になさそうだったのと勉強のために 1 から作っていきます。
Elixir について
ここでは特に説明はいらないとも思いますが、Erlang VM 上で動作する Ruby 似のシンタックスを持っている関数型言語です。
インストール方法はこちら。
http://elixir-lang.org/install.html
詳しい説明は Qiita 上にも沢山の素晴しい記事があるので省略。
Elixir は今年ずいぶんと話題になった言語だと思います。
かく言う私もその話題に乗って触り始めたにわかです。
SuperCollider について
SuperCollider (以下 SC ) を簡単に説明すると、リアルタイム音響合成とアルゴリズミック・コンポジションに特化した音響合成用プログラミング環境および言語です。
今回はこのリアルタイム音響合成の部分を使います。
内部のアーキテクチャはクライアントとサーバに分かれています。付属のエディタクライアントでコードを書いて実行、その実行内容を解釈し実際に音が鳴るところがサーバとなっています。そのクライアント/サーバ間では UDP / TCP を利用した Open Sound Control というプロトコルで通信を行います。
詳しくはこちら。
この仕組みにより、元々 SuperCollider に組み込まれているエディタクライアント以外からでもサーバをコントロールしやすくなっています。
ダウンロード & インストール
今度は SC 側の準備です。まず SC をこの辺からダウンロード & インストール。
http://supercollider.github.io/
起動 & 実行
起動すると以下の様な画面が開きます。
この場合画面左側がエディタになっており、ここに以下のようにコードを書いていきます。
s.boot; // SC サーバ起動
{ SinOsc.ar(1000) }.play; // 1000Hz のサイン波を再生
// Command + . で停止
コードの実行は、Command + Enter で、() でくくられているブロックであればそのブロック全体、単行であればその行を実行します。
Command + . で現在実行中の諸々をストップします。
SynthDef
SC では SynthDef で以下のように楽器を定義出来ます。
先程 {...}.play として再生させましたが、それに名前を付けるようなイメージです。
s.boot; // SC サーバ起動
(
SynthDef(\kick01, {|amp=0.6, dur=0.8|
var env1, env2, out;
env1 = EnvGen.ar(Env.perc(0.001, dur, 1, -4), doneAction:2);
env2 = EnvGen.ar(Env.new([6000, 300, 20], [0.001, 0.2], [-4, -5]));
out = SinOsc.ar(env2, 0) * env1;
out = out * amp;
Out.ar(0, out.dup);
}).add;
) // \kick01 という名前で楽器を定義
Synth(\kick01); // \kick01 を鳴らす
Open Sound Control とは
Open Sound Control (以下 OSC) とは、いわゆる MIDI の代替となるべく考案されたプロトコルで、
- MIDI の通信方式と比べより一般的な UDP や TCP を利用した通信
- MIDI より柔軟に構築出来るメッセージ構造
というのが特徴です。
仕様はこちら。
http://archive.cnmat.berkeley.edu/OpenSoundControl/OSC-spec.html
これが SC で標準的に実装されているために他のプログラミング言語との連携が容易になっています。
ただ、考案されておそらく20年弱、特に MIDI の代替とはなっておらず、未だ音楽通信プロトコルのあたりまえにはなっていません。残念。
Elixir から SC への通信
TCP や UDP が使えるとなると、OSC のメッセージを構築し通信出来る実装さえあれば SC サーバをコントロールすることが出来ることになります。
ということで、まずは Elixir の OSC クライアントを作成。
https://github.com/reprimande/exosc
メッセージの構築の部分はこのような実装になっていて、今回必要な型のみの実装ですが、Elixir のパターンマッチ / パイプライン演算子 / ガード句を使って、結構簡潔に送信するバイナリを構築出来てると思います。
defmodule OSC.Message do
def construct(path, args) do
padding(path <> <<0>>) <> parse_args(args)
end
def parse_args(args) when is_list(args) do
{ tags, values } = args
|> Enum.map(fn (x) -> parse_value(x) end)
|> Enum.unzip
t = [",", tags, <<0>>] |> List.flatten |> Enum.join |> padding
v = values |> Enum.join
t <> v
end
def parse_args(args), do: parse_value(args)
def parse_value(value) when is_integer(value) do
{ "i", <<value :: big-signed-integer-size(32)>> }
end
def parse_value(value) when is_float(value) do
{ "f", <<value :: big-signed-float-size(32)>> }
end
def parse_value(value) when is_binary(value) do
{ "s", padding(value <> <<0>>) }
end
def padding(buf) when rem(byte_size(buf), 4) == 0, do: buf
def padding(buf), do: buf <> <<0>> |> padding
end
送信クライアント部分は Erlang の UDP モジュールを利用して GenServer で実装。
defmodule OSC.Client do
use GenServer
def send(ip, port, path, args) do
data = OSC.Message.construct(path, args)
GenServer.cast(:osc_client, {:send, ip, port, data})
end
# API
def start_link do
GenServer.start_link(__MODULE__, :ok, name: :osc_client)
end
# Callback
def init(:ok) do
:gen_udp.open(0, [:binary, {:active, true}])
end
def handle_cast({:send, ip, port, data}, socket) do
:ok = :gen_udp.send(socket, ip, port, data)
{:noreply, socket}
end
end
で、それを SC 向けにラップしたものも作成。
https://github.com/reprimande/exsc3
実装はたいしたコードではないのでリンク先参照。
ここまでで作成したもので Elixir から SC を鳴らしてみましょう。
git clone https://github.com/reprimande/exsc3.git
cd exsc3
mix deps.get
iex -S mix
iex> SC3.Server.start_link
{:ok, #PID<0.94.0>}
iex> SC3.Server.send_msg("s_new", ["kick01", 1000, 1, 0])
:ok
先の SynthDef で定義した "kick01" が鳴ります。
これで Elixir から SC をコントロールする準備は整いました。
Elixir で曲みたいのを作る
Elixir は Erlang ゆずりのアクターモデルが特徴の一つであり、その仕組みを利用して曲を奏でるよう実装してみます。
大体こんな感じ。
奏でてみよう
作って試してみているコード達はこちら。
https://github.com/reprimande/ex_music_sandbox
雑な作りで突っ込みどころも多々ありますが、今の実力はこんなものなので気にせず先へ進めます。
SC
今回使う楽器群( SynthDef )
synthdefs/synthdefs.sc
これは前述の SC のエディタで実行しておきます。
Elixir
Clockモジュール
メトロノームのように動作するクロック。
Erlang の timer モジュールの interval を利用して定期的にイベントを発行しています。
interval でディスパッチされたイベントを GenEvent.stream を使って受けとり、指定リスナー(プロセス)に GenServer.cast でメッセージを送ってます。
以降とりあえずイベント周りはこんな感じで実装してますがもっと良い実装がありそう。
defmodule Clock do
use GenServer
...
def start_link(ms \\ 1000) do
{:ok, event} = GenEvent.start_link
GenServer.start_link(__MODULE__, [ms, event])
end
def add_tick_handler(pid, listener) do
GenServer.cast(pid, {:add_tick_handler, listener})
end
def _timer_interval(event) do
GenEvent.notify(event, {:tick})
end
def handle_cast({:add_tick_handler, listener}, {ms, event}) do
Task.start_link(fn ->
for e <- GenEvent.stream(event) do
GenServer.cast(listener, e)
end
end)
{:noreply, {ms, event}}
end
def handle_cast({:start_timer}, {ms, event}) do
:timer.apply_interval(ms, __MODULE__, :_timer_interval, [event])
{:noreply, {ms, event}}
end
...
end
ステップシーケンサ
List か Function をステップ毎に処理して値をメッセージ送信するシーケンサ。
List だと順番にループ処理し、Function だと都度実行しその戻り値をそのステップの値としてます。
defmodule StepSequencer do
use GenServer
def start_link(pattern, div \\ 1) do
GenServer.start_link(__MODULE__, [pattern, div])
end
def init([pattern, div]) do
{:ok, event} = GenEvent.start_link
{:ok, {event, pattern, [], 0, div}}
end
...
def handle_cast({:tick}, {event, pattern, [], step, div}) when is_list(pattern) and rem(step, div) == 0 do
GenEvent.notify(event, hd(pattern))
{:noreply, {event, pattern, tl(pattern), step + 1, div}}
end
def handle_cast({:tick}, {event, pattern, [val|rest], step, div}) when is_list(pattern) and rem(step, div) == 0 do
GenEvent.notify(event, val)
{:noreply, {event, pattern, rest, step + 1, div}}
end
def handle_cast({:tick}, {event, func, [], step, div}) when is_function(func) and rem(step, div) == 0 do
GenEvent.notify(event, func.(step))
{:noreply, {event, func, [], step + 1, div}}
end
...
end
楽器を鳴らすモジュール (例: Kick)
:trigger というメッセージを受け取り、SC の kick01 を鳴らすコマンドを送信します。
似たような楽器モジュールがいくつか出来たので、ベースモジュール的なものも作成しています。
defmodule Synth.Perc do
defmacro __using__(_opts) do
quote do
use GenServer
def synth_name do "default" end
def start_link() do
GenServer.start_link(__MODULE__, [])
end
def init(_) do
{:ok, {synth_name}}
end
def play(pid) do
GenServer.cast(pid, {:play})
end
def handle_cast({:play}, { name }) do
SC3.Server.send_msg("s_new", [name, SC3.Server.get_node_id, 0, 0])
{:noreply, {name}}
end
def handle_cast({:trigger, 1}, { name }) do
SC3.Server.send_msg("s_new", [name, SC3.Server.get_node_id, 0, 0])
{:noreply, {name}}
end
def handle_cast({:trigger, _}, { name }) do
{:noreply, { name }}
end
defoverridable [synth_name: 0]
end
end
end
defmodule Kick do
use Synth.Perc
def synth_name do "kick01" end
end
曲の構造を記述し、上記のプロセス群を管理する Supervisor
defmodule AcidSup do
use Supervisor
...
def start_link do
{:ok, sup} = Supervisor.start_link(__MODULE__, [])
{:ok, clock} = Supervisor.start_child(sup, worker(Clock, [Clock.bpm2ms(135, 4)]))
[
{ "s1", Kick, [1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,1,1] },
{ "s2", Clap, [0,0,0,0, 0,1,0,0, 0,0,0,1, 0,0,1,0] },
{ "s3", HiHat, [1,1,1,0, 1,1,1,1, 1,0,1,1] },
{ "s4", Bass, [24,36,48,36, 0,24,48,60, 24,48,0,36, 60,60,0,60] }
] |> Enum.each(fn({n, m, p}) ->
{:ok, inst} = Supervisor.start_child(sup, worker(m, [], id: n <> "_inst"))
{:ok, seq} = Supervisor.start_child(sup, worker(StepSequencer, [p], id: n <> "_seq"))
Clock.add_tick_handler(clock, seq)
StepSequencer.add_step_handler(seq, inst, :trigger)
end)
Clock.start(clock)
{:ok, sup}
end
def init(_) do
supervise([worker(SC3.Server, [])], strategy: :one_for_one)
end
...
end
end
実行してみます。
git clone https://github.com/reprimande/ex_music_sandbox.git
cd ex_music_sandbox
mix deps.get
iex -S mix
iex> {:ok, pid} = AcidSup.play # 再生
...
iex> AcidSup.stop(pid) # 停止
奏でられました。
https://soundcloud.com/naokinomoto/acid-elixir-played/s-SZgfF
自動作曲してみよう
プログラミングで音楽を作れるということは、乱数やアルゴリズムを利用した生成的な音楽を作れます。
ロジスティック写像を利用したメロディの生成し、雑なマルコフ連鎖でコード進行を生成、ステップ毎の確率でビートを生成するという、自動作曲っぽいのもやってみます。
長くなってきたので実装は各リンク先を参照。
各ロジックのモジュールでは状態を扱うのに Agent を利用しています。
- lib/util/logistic_map.ex
- lib/util/markov.ex
- lib/util/prob.ex
- lib/util/midi_util.ex
- lib/piece/generative_sup.ex
iex> {:ok, pid} = GenerativeTestSup.play # 再生
...
iex> GenerativeTestSup.stop(pid) # 停止
出来た曲はこちら。
https://soundcloud.com/naokinomoto/algo-music-elixir-played/s-c9Zrz
響きやビートが不細工なところもありますが、パターンがループしてるようなものではなく常に曲が変化してるのがなんとなく分かるかなと思います。
タイムリーにも VisualixirでElixir/Erangノード内部を覗いてみる で紹介されていた Visualixir が面白かったので、この曲のプロセスのグラフも表示させてみたのがこちら。
クリスマスソングを奏でよう
最後は Advent Calendar ですしあざとくこんな曲で。
lib/piece/mcml_sup.ex
iex> {:ok, pid} = McmlSup.play # 再生
...
iex> McmlSup.stop(pid) # 停止
まとめ
Elixir でも無事音楽を奏でられ、大変良い勉強になりました。
全体的にもっと良い実装が出来るはずなのでもうちょっと勉強して整理したいです。
また、例えば Emacs には alchemist という iex といい感じに統合してる拡張モードもあったりするので、ライブをすることも頑張れば出来るかもしれません。
Elixir 楽しい。アクターモデル楽しい。
余談
これをやるのに調べてて見つけた Erlang 作者 Joe Armstrong 氏の Blog 記事。
Erlang + Sonic Pi で似たようなことやっててちょっとうれしい。
http://joearms.github.io/2015/01/05/Connecting-Erlang-to-Sonic-Pi.html