読者です 読者をやめる 読者になる 読者になる

音の鳴るブログ

鳴らないこともある

音楽用CoffeeScriptを作ろう (15) Taskを書き直した

Demo - CoffeeCollider

Task っていうのは非同期でスレッドのように動作する関数で以下のように書くと、ラの音を出してコンソールに出力する → 1秒後にミの音を出してコンソール → 1秒後に最初に戻ってラ。っていうのを8回繰り返すみたいな処理を書くための記法兼クラスです。

Task.do 8, ->
  synth.play freq:440
  console.log 'ラ'
  @wait 1
  synth.play freq:660
  console.log 'ミ'
  @wait 1
.play()

こういう機能は時間ベースで制御したい音楽プログラミングでは必須だと思うので結構初期から書いていたのだけど、最初に書いたのは特定のメソッドは実行するとタイムラインにいったん保存されて遅延実行されるというものだった。さっきの例でいうと synth.play は1秒待つけど console.log は2つが同時に表示されるという動作をしていた。こういうやり方だと書く方は wait の影響を受けるメソッドか受けないメソッドかを覚えないといけないし、処理する方も方々に依存関係を作って複雑でメンテナンスしにくそうだと思ってガガーーっと書き直してTaskの中はあらゆるメソッド呼び出しは wait の影響を受けるようになった。コードもかなりシンプルになった。

やっていることは単純で関数を以下のように分割して書き直して順番に実行しているだけ。@wait 以外に @continue, @redo, @break が使えるようになったのも良い。

Task.do 8, ->
  [
    ->
      synth.play freq:440
      console.log 'ラ'
      @wait 1
    ->
      synth.play freq:660
      console.log 'ミ'
      @wait 1
  ]
.play()

しかし問題があって制御文 (if とか for) の中で @wait 等が使えない。

たぶん制御文の中身を上記の分割した配列に展開してプログラムカウンタ的なもので次はどこを実行するか飛び飛びに指定することができれば解決しそうだと思っているのだけど、今の時点では知識も能力も足りない。とりあえず最初のバージョンをリリースするためには妥協も必要だと思うのだけど、基本の部分なので出来ないことがつらすぎて泣きそう。

ただし一応 Task のネストには対応していて書く人が頑張れば if も for も可能にはなっている。最初にあげたサンプルコードでは無限ループのなかに for ループがある構成になっているけど、やっぱり書くのが難しくなって良くないと思う。

実際にどのようにコードを書き直ししているかはデモページの [Coffee] ボタンを押すとウェブコンソールに表示される。コードをよく見ると気がつくかもしれないけどタスク内のプライベート変数や、外側の変数を内側で使うときに外側の影響を受けないように等々、色々工夫している。結構工夫したとか言いながら制御文使えませんっていうの格好わるい。


この記事に対するコメント (読み返して思った)

プログラムカウンタ的なもので飛び飛びに実行させるのはお前の技術力では難しかろう。Task のなかの制御文をサブTaskとして書き直せば大きな修正は必要ないのでは?

# origin
Task.do ->
  a = for i in [60..72]
     synth.play freq:i.midicps()
     @wait 1
     i
  console.log a.length
.play()

# replaced
Task.do ->
  @wait (a = Task.each([60..72], (i)->
    synth.play freq:i.midicps()
    @wait 1
    i
  ).play())
  # でもこれだとこの時点の a が TaskProcessorEach になって
  console.log a.length # ここで12を出すのが難しそう..
.play()