Edited at

Elixir と SuperCollider で音楽を奏でてみる

More than 3 years have passed since last update.

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 というプロトコルで通信を行います。

詳しくはこちら。

http://doc.sccode.org/Guides/ClientVsServer.html

この仕組みにより、元々 SuperCollider に組み込まれているエディタクライアント以外からでもサーバをコントロールしやすくなっています。


ダウンロード & インストール

今度は SC 側の準備です。まず SC をこの辺からダウンロード & インストール。

http://supercollider.github.io/


起動 & 実行

起動すると以下の様な画面が開きます。

sc.png

この場合画面左側がエディタになっており、ここに以下のようにコードを書いていきます。

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 のパターンマッチ / パイプライン演算子 / ガード句を使って、結構簡潔に送信するバイナリを構築出来てると思います。


lib/osc/message.ex

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 で実装。


lib/osc/client.ex

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 を鳴らしてみましょう。


shell

git clone https://github.com/reprimande/exsc3.git

cd exsc3
mix deps.get
iex -S mix


iex

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 ゆずりのアクターモデルが特徴の一つであり、その仕組みを利用して曲を奏でるよう実装してみます。

大体こんな感じ。

elixir-sc.png


奏でてみよう

作って試してみているコード達はこちら。

https://github.com/reprimande/ex_music_sandbox

雑な作りで突っ込みどころも多々ありますが、今の実力はこんなものなので気にせず先へ進めます。


SC


今回使う楽器群( SynthDef )

synthdefs/synthdefs.sc

これは前述の SC のエディタで実行しておきます。


Elixir


Clockモジュール

メトロノームのように動作するクロック。

Erlang の timer モジュールの interval を利用して定期的にイベントを発行しています。

interval でディスパッチされたイベントを GenEvent.stream を使って受けとり、指定リスナー(プロセス)に GenServer.cast でメッセージを送ってます。

以降とりあえずイベント周りはこんな感じで実装してますがもっと良い実装がありそう。


lib/time/clock.ex

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 だと都度実行しその戻り値をそのステップの値としてます。


lib/sequencer/step_sequencer.ex

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 を鳴らすコマンドを送信します。

似たような楽器モジュールがいくつか出来たので、ベースモジュール的なものも作成しています。


lib/synth/perc.ex

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



lib/synth/kick.ex

defmodule Kick do

use Synth.Perc
def synth_name do "kick01" end
end


曲の構造を記述し、上記のプロセス群を管理する Supervisor


lib/piece/acid_sup.ex

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


実行してみます。


shell

git clone https://github.com/reprimande/ex_music_sandbox.git

cd ex_music_sandbox
mix deps.get
iex -S mix


iex

iex> {:ok, pid} = AcidSup.play # 再生

...
iex> AcidSup.stop(pid) # 停止

奏でられました。

https://soundcloud.com/naokinomoto/acid-elixir-played/s-SZgfF


自動作曲してみよう

プログラミングで音楽を作れるということは、乱数やアルゴリズムを利用した生成的な音楽を作れます。

ロジスティック写像を利用したメロディの生成し、雑なマルコフ連鎖でコード進行を生成、ステップ毎の確率でビートを生成するという、自動作曲っぽいのもやってみます。

長くなってきたので実装は各リンク先を参照。

各ロジックのモジュールでは状態を扱うのに Agent を利用しています。


iex

iex> {:ok, pid} = GenerativeTestSup.play # 再生

...
iex> GenerativeTestSup.stop(pid) # 停止

出来た曲はこちら。

https://soundcloud.com/naokinomoto/algo-music-elixir-played/s-c9Zrz

響きやビートが不細工なところもありますが、パターンがループしてるようなものではなく常に曲が変化してるのがなんとなく分かるかなと思います。

タイムリーにも VisualixirでElixir/Erangノード内部を覗いてみる で紹介されていた Visualixir が面白かったので、この曲のプロセスのグラフも表示させてみたのがこちら。

スクリーンショット 2015-12-15 19.47.40.png


クリスマスソングを奏でよう

最後は Advent Calendar ですしあざとくこんな曲で。

lib/piece/mcml_sup.ex


iex

iex> {:ok, pid} = McmlSup.play # 再生

...
iex> McmlSup.stop(pid) # 停止

https://soundcloud.com/naokinomoto/mxxxx-cxxxxxxxx-mxx-lxxxxxxx-elixir-played/s-4BhP1


まとめ

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