LoginSignup
60
56

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-12-17

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/

起動 & 実行

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

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) # 停止

まとめ

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

60
56
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
60
56