概要
題名のとおりです。ちなみに Sigourney の作者は Golang 開発者の1人である Andrew Gerrand さんです。
リポジトリ: https://github.com/nf/sigourney
注意: 音量に注意してください。操作するときは予め音量を絞っておくことをお勧めします。
使ってみる
以下のコマンドで起動します。
$ go get github.com/nf/sigourney
$ cd $GOPATH/src/github.com/nf/sigourney
$ sigourney
cd コマンドでディレクトリに移動するのを忘れないで下さい。
コマンドを叩くとブラウザが立ち上がります。立ち上がらない時は http://localhost:8080/
に直接接続してください。
操作用のGUIはブラウザで提供され、操作内容はWebSocketを通じてバックエンドで動いているSigourneyの本体に送られます。
WebSocketで受け取ったメッセージはシンセサイザー本体で処理され、最終的な音声データはPortAudioを通じて外部に出力しています。
起動まで
コマンドを叩いてからブラウザが立ち上がるまでのコードを読んでいきます。
main.go
まず main.go で net/http
と gorilla/websocket
を利用して、ブラウザからの操作を待ち受けます。
大まかな処理の流れは以下の通り。
- 引数のフラグ解析
- PortAudioの初期化
- deferでClose
- PortMidiの初期化
- deferでClose
- httpサーバーの準備
-
socket.Handler
関数を HandlerFunc に登録
-
- goroutine で httpserver を動かす
- os.Stdin で入力待ちの状態にする。
ウェブソケットは socket/socket.go
の Handler
関数で処理されます。
socket/socket.go
socket.go には main.go で呼ばれる websocket処理用の関数 Handler
があります。
この関数の処理流れは大体以下のとおりです。
参照: socket.go
- gorilla/websocket でWebSock受け取り
- NewSession で
Session
構造体 を生成、このときui.UI
構造体もつくられる。 - goroutine で ws の書き込みチャネルからのデータを待ち受ける無限ループを生成
-
session.M
の読み込み専用チャネルを待つ - 書き込みあった
r
を,c.WriteJSON
(受け取ったメッセージ)で書き込む - JSONを書き込み、ブラウザに描画命令をウェブソケットで送る
-
- Handler関数内で読み込みの無限ループ発生させる
- goroutine化しない
-
new(Message)
で新しいメッセージを生成し -
ws.ReadJSON
でws
からのデータ連絡を待つ - s.Handle で受け取ったメッセージを処理する
構造体の整理
ここまで登場した構造体の役割をざっと整理します。
NewSession では Session構造体とUI構造体の2つを新規作成しています。この2つの構造体はSigourney内で以下の役割を持ちます。
- Session
- ブラウザとSigrouney(シンセ本体)とのメッセージ管理
- ウェブソケットから送られてくるメッセージを受け取り、シンセ本体へ命令を中継する
- UI
- シンセ本体の各モジュールを管理する構造体
- 各モジュールとは、ブラウザに表示されている箱のこと
- シンセ本体の各モジュールを管理する構造体
NewSession関数の見れば分かる通り、1つのSession構造体に1つのUI構造体が登録されます。
Sessionはブラウザからメッセージを受け取り、登録されているUI構造体へ中継します。
UI構造体は受けとったメッセージをパースし、モジュール操作を行います。
各シンセモジュールはUI構造体からの命令に従い、接続や切断の処理を行います。
では次にUI構造体を見てみます。
UI構造体は以下のプロパティを持ちます。
- 自身を生成したSession構造体
h
- 生成されたシンセモジュール達のポインタを保存するマップ
objects
- オーディオ出力を担当する
engine
UI構造体はSet
やらConnect
などの関数で objects
(シンセモジュール)に対して操作を行います。
これは、上で説明したSession構造体のHandler関数で受けとったメッセージによって s.u.Connect のように呼び出されます。
また、UI構造体は Engine
という Object
が同時に生成されます。
Engineは特別なモジュールで、UI構造体の生成時(つまりSigourneyの起動時) に一つだけ生成されます。Engineはユーザーの操作で作成したり削除することはできません。
Engineはその名の通り音声出力を担当するモジュールで、各モジュールが行う操作は最終的にUI構造体が生成したEngineを通して外部に出力されます。
なので、UI構造体は各シンセモジュールに対する操作の他に、それらのモジュールを管理する役割も含まれます。
以上より、おおまかに以下のような関係になります。
ブラウザ → WebSocket → Session → UI → Object → UI.engine → 音が出る
起動後
ざっと起動処理をみたので、今度は実際のユーザー操作を追いかけながら、中のコードを読んでいきます。
起動時の画面は以下のようになります
Chrome Developer Tools でウェブソケットの通信を見てみます。
最初にサーバー(Sigourney)からハンドシェイクメッセージが来ています。
{"Action":"hello","From":"","KindInputs":{"clip":["in"],"delay":["in","len"],"engine":["in"],"env":["att","dec","gate","trig"],"gate":null,"mul":["a","b"],"noise":null,"note":null,"quant":["in"],"rand":["max","min","trig"],"sequencer":["rst","trig","v0","v1","v2","v3"],"sin":["pitch","syn"],"skip":["num","trig"],"square":["pitch","syn"],"sum":["a","b"],"value":null},"Message":""}
このメッセージはこの行で送られています。
次に画面上に Engine
を描画する命令(実際には描画したという記録)がブラウザから送られます。
{"Action":"setDisplay","Name":"engine","Display":{"offset":{"top":419.25,"left":505}}}
前述したとおり、Engine
はUI構造体の生成時に自動で作られるので、Sigrouney立ち上げ時には、すでに内部で存在しています。
ですので、ブラウザ立ち上げ時に描画命令がお約束的に送られます。
- Sinモジュールの設置
次にSinモジュールをおいてみます。
以下の命令がブラウザからサーバーへ送られます。
{"Action":"new","Name":"sin2","Kind":"sin"}
{"Action":"setDisplay","Name":"sin2","Display":{"offset":{"top":215,"left":493}}}
1つ目はSigourney内部でSin波のシンセモジュールを生成する命令。中で実際に呼ばれるのは 2つ目は前と同じくブラウザでの描画伝達です。
この2つの命令がどう処理されるかコードを読んでみます。
まず session.go
の Handler
を経由して命令が UI構造体に送られます。
参照: socket.go#L149
new
が送られた場合は新しくSin波を生成するシンセモジュールが作られます。
参照: ui.go#L198
周波数のピッチを設定して音程を低くしたSin波を出力するパッチを以下のように作った場合、送られる命令は以下の通りです。
同じく {"Action": "hoge"..}
の部分をみて個別の処理を行います。
- value 設置して0.1 に設定
{"Action":"new","Name":"value3","Kind":"value","Value":0}
{"Action":"setDisplay","Name":"value3","Display":{"offset":{"top":73,"left":490}}}
{"Action":"set","Name":"value3","Value":0.1}
- Sin と value 接続
{"Action":"connect","From":"value3","To":"sin2","Input":"pitch"}
- Sin と Engine を接続
{"Action":"connect","From":"sin2","To":"engine","Input":"in"}
上記の操作を行うと、最終的に低めの音程のSin波がスピーカーから出力されると思います。
実際にどうやって音が出てるのか
上記の操作で実際にどのように音が出されていくのかを見ていきます。
音声出力は PortAudio の機能を利用しています。実際に機能を呼び出しているのは前述した Engine
構造体です。
Engine
構造体はSigourney の audioパッケージに定義されています。
このパッケージには以下のソースがあります。
- engine.go
- Engine構造体の定義
- engine_cgo.go
- PortAudio(Cライブラリ)の機能を使うEngine構造体のメソッド定義はここにあります。
- common.go
- シンセモジュールを
Object
構造体で共通化するためのInterfaceの定義- 各シンセモジュールの提供する機能をInterfaceで区別して管理する
- 区別する機能とは、接続・切断・データの出力など
- 各シンセモジュールの提供する機能をInterfaceで区別して管理する
- シンセモジュールを
- proc.go
- 各シンセモジュールの定義
-
Sin
とかの実際に行う処理が記述されている
- dup.go
- 複数の出力先に対応するための構造体定義
- 例えば
value
は複数の入力先に接続できる
Engine それ自体は特別に設計されたものではなく、Sin や Value などのと同じくモジュールの一種として定義されています。
これらのモジュールには audioパッケージで定義された構造体がふんだんに使われています。
構造体の説明
audioパッケージで定義されている構造体は、Sigourney の処理の中核となるものです。その定義や役割をコードを読んで整理します。
- Processor インターフェース
- io.Readerを模した音声データ読み込み I/F
- byte を読み込む
- Ticker インターフェース
- Engine構造体によって管理されるデータ読み込みの協調動作管理用インターフェース
- 複数のシンセモジュールが、過剰に音声データを生成しないように、タイミングを管理する役割
- Processor は自身の処理を終えてバッファを返すと、自身のフラグをtrueにする
- Tikcerを満たすは Dup構造体
- つまり実際に音声データを生成する構造体のみ
- Tickerを満たす構造体は、すべて Engine に管理される
- ui.NewObject関数内で
AddTicker
という関数が呼ばれている - Engine は音声データを出力し終わったら自身が管理しているTickerのTickを呼び出し、フラグをfalse に戻す
- ui.NewObject関数内で
- sink 構造体
- データ入力部分の管理
- 表示される箱の入力部分を管理するもの
- sink の inputs map は ui.Connect時に必要で、それ以外では使わない
- 各オブジェクトが直接 e.in.Process とか呼び出してる
- 音声データに操作を「与える」側
- source 構造体は Procesメソッドを持つが、Processor I/F を満たさない
- Processor は操作を受ける側
- Dup 構造体
- 音声出力を担当する
- 各シンセモジュールは複数の接続先を持てる
- buf は2つ以上に接続するときのために、コピーを渡しデータに齟齬が内容にする
- outs に実際に出力するさきのオブジェクトへの参照が入る
- ui.Connect で
f.dup.Output()
で実際の出力先を取得している- この返り値が対象の
t.proc.(audio.Sink).Input(input, o)
で入力値として参照される
- この返り値が対象の
- なので、実際に接続されると音が出るまでに
-
object.Dup.Output.Process()
という流れがある
-
実際に音を出す部分、、の前に
Engine
が音声を出力するためには、入力からなんからの音声データを取得しなければなりません。
そのためには他のモジュールを設置し、それと接続される必要があります。
よって、モジュール同士の設置と接続がどのように行われるかを見てみます。
設置処理
接続するためには、対象となるモジュールが必要です。まず設置処理を見ていきます。
実際にSinモジュールがどのように生成されるかをざっくり読んでいきます。
{"Action":"new","Name":"sin2","Kind":"sin"}
- まず初期化のときに、sink.input が呼ばれる。
- 例えば、Sinなら NewSin で呼ばれる
- sink 構造体はほぼすべてのObjectに埋め込まれている
- sink のメンバである
m[string]interface{}
は描画される箱の上部分の入力端子が収まる - Engine なら
m{"in":interface{}}
という感じ-
interface{}
には接続元のオブジェクトの参照が保存される
-
- また sink構造体を埋め込むことによって、そのオブジェクトは Sink I/F を満たす
- 生成された
Sin
は Object構造体の proc に登録される- Object構造体の実際の処理は
proc
が担当する - 生成された
Sin
モジュールは UI構造体のOjbects
メンバで管理される
- Object構造体の実際の処理は
接続処理
接続処理は前述のとおり UI.Connect で行われます。この関数は色々な構造体/インターフェースが出てきて詳細に説明するのは大変なのでざっくりと書いていきます。
- ウェブソケットのメッセージから、接続されるモジュールの出力と入力を特定する
- 特定した情報をもとに
UI.Objects
からモジュールの参照を取得 - 接続する側(from) は
Object.dup.Output
を呼び出し出力先の複製を作成する- この複製は元のオブジェクトの
proc
への参照を保持しているので、出力されるデータは全て同じになる
- この複製は元のオブジェクトの
- 接続される側(to) は from が作成した出力の複製(への参照) を
sink.input
に保存する-
Sink.Input()
で対象の Output の参照が map に保存される - これで2つのモジュールが
Object.dup.Output
とObject.(Sink).Input
で参照しあうことで接続が完了したこになる
-
実際に音がでる仕組み
まず終点から見ます。PortAudioは音声データを非同期コールバック関数によって処理します。
音声データが送られてきたり、逆に無くなったりした時に、非同期でコールバック関数を呼び出し音声データの取得ないし生成を行うという仕組みです。
Sigourney では PortAudioをcgoで呼び出します。
- コールバック関数の登録
- コールバック関数の定義
上記のコールバック関数の定義の中で e.Process()
という関数から音声データが得られていることがわかります。
なので Engine構造体の Process 関数を見てみます。
参照: engine.go#L50
今度は e.in.Process
という関数が呼ばれています。
Engine構造体の定義より in
は common.go
に定義されている source
構造体です。
source.Process で、さらに参照先の Process()
を呼び出します。
呼び出されたオブジェクトは自身の処理中に、また接続されたProcess を呼び出されます。
例えば接続されたモジュールがSin
ならSin.Process
のなかで(何か入力されていた場合)接続されているモジュールのProcessを呼び出します。
よって、Engine.Process
を起点として、入力側のProcessを呼び出し、さらにその入力側のProcessを呼び出し…、として、末端までデータを取得していき、それが終われば値を加工して返して、値を加工して返して…、と来た道を戻っていきます。
こうして最終的に取得された音声データがEngineに渡され、音が鳴ります。めでたし。
言葉だけだとわかりにくいので、図にしてみました。
黒い矢印が実際のデータの流れですが、データ取得は赤い点線で示したように Engine
のProcess()
を起点として上へ上へとのぼっていきます。
赤い線が末端まで言ったら、再度黒い線の流れにのって取得したデータがEngineまで流れていきます。
余談: Windows
Windows でも動かせますが、go get
で PortAudio、PortAudio がうまく入らないので、自前でビルドする必要があります。
PortAudio/PortMidi をそれぞれ、VisualStudio/MinGWなどでコンパイルし、dll を生成します。
生成した dll を $GOROOT/src/github.com/nf/sigourney
のフォルダに置いて、プロンプトから sigourney.exe
を叩けば Windows でも動作します。
まとめ
読み解くのは難しかったが、モジュラーシンセサイザーという複雑なソフトウェアを適切なIntarfaceやstructを用いて綺麗に設計していると感じた。流石Goの開発者だと思った。
複雑な機能をもつオブジェクトの設計の参考にしたい。