Home > Tags > 206

206

LeopardでのQuartzComposerカスタムプラグインづくり

Ruby会議2008CFPとして、RubyCocoaでインタラクティブにMacのプラグインをつくるよ、みたいなものを書いて出してみました。 出してから、そういえばLeopardになってからQuartzComposerで遊んでないなあと気づき、ちょっと触ってみたらえらく変わっていたので、今日調べたことをカスタムプラグインをつくるという観点でメモっときます。

カスタムプラグインがオフィシャルに

最大の変更点はこれです。 Appleの丁寧なドキュメント Introduction to Quartz Composer Custom Patch Programming Guide を読めば、たちまちつくれるようになるのではないかと。サンプルも充実してて、/Developer/Examples/Quartz\ Composer/Plugins/にごろごろ転がってます。 これまで、kineme.net:Xcode Template for Custom Quartz Composer Patches を使い、非公開のAPIでプログラミングしていたのに対して大きな進歩です。

Port指定にはpropertyをつかう

Objective-C 2.0で導入された、propertyという機能をつかって入出力Portの指定をコードでやるようになっています。 これまでは、インスタンス変数名の先頭にinput/outputがあれば、それがPortになっていました。

Portの増減が動的にできる

RubyCocoaでpropertyを使う方法を知らないのでどうしようかなあと思ってたのですが、 addinputPortWithTypeとかaddOutputPortWithTypeというメソッドを使えば動的にPortが加えられます。

注意すべきことは、これらのメソッドはプラグインのexecuteメソッドに入れてはいけないことです (例外があがってQuartzComposerが止まってしまいます)。んじゃどうするかということで、ExampleのCommandLineToolFreeFrameHostを調べてみると、どうやらQuartzComposerのインスペクタから受け取るイベントのハンドラで、これらのメソッドを呼ぶようにしています。なるほど。

インストール場所が変わっている

これまでは、/Library/Graphics/Patch にプラグインを置くのが流儀だったのですが、Leopardからは/Library/Graphics/Quartz\ Composer\ Plug-Ins/ になりました(~/Library以下でもOK)。


カスタムプラグインをつくるという点から、Leopardになって気づいたことをまとめました。次はいよいよ、RubyCocoaでカスタムプラグインづくりです。

関連エントリ

RubyCocoa で QuartzComposer CustomPatch (2)

とりあえず、できました。

スクリーンショット

QCRubyPatch QCRubyPatch-2

サンプル

input, output ともにStringのPortを1つずつもち、入力にある”Objective-C”を”Ruby”に正規表現で置換するというだけのサンプルを書きました。

RubyPatch (binary & source) rev3 (i386 Binary)

RubyCocoaをUniversal Binary でビルドしていなかったため、いまのところIntel Mac だけで動きます。

RubyCocoa の svn trunk、 MacBook Kuroで動作確認をしています。

何をしているか

  1. Patchがロードされるとき、すなわち “registerNodesWithManager” が呼ばれるときに、RBBundleInitを呼んでRubyCocoaの準備をする
  2. Patchの動作を記述するObjective-CのクラスをProxyとして、実際の動作を記述するRubyクラスのインスタンスを保持させる
  3. Rubyクラスは、Objective-CクラスからPortをもらってくる
  4. Patchを実行するイベントがきたら、Rubyクラスのインスタンスに対して処理を移譲

といったところ。

実際のコードは、RubyQC::Trac にあるのでご覧ください。

このアプローチの問題点

  • いちいち全メソッドを Objective-Cのクラス → Rubyのクラス に投げるよう書くのがめんどう
  • Portの記述がObjective-Cのクラスにしか書けない (みたい) なので、Portを加えたり減らしたりするたびにObjective-C/Rubyコードともに変更しないといけない。わすれそう。

OSX::QCPatchクラスをRubyクラスで継承できて、さらにPortの情報もRubyクラスで書けるようになれば、これらの問題は解決しそうです。

課題

QCPatch.execute は、3つの引数をとるのですが、Rubyクラスのメソッドにどうやって3引数を渡すのか分からない…

18:03 追記

MLで、LimeChatの中の人に教えてもらいました。

objc_method :execute_time_arguments, %w|char id double id|
def execute_time_arguments(fp8, fp12, fp20)
  ...
end

のようにすればいいよ、とのことで、ばっちり動きました。ありがとうございます。


とはいえ、CustomPatch がRubyで書けるようになったのは大きな進歩です。ApacheReaderPatch とかも、ずいぶんと簡単に書けそう。

RubyCocoa で QuartzComposer CustomPatch をつくりたい

先に今の状態 : できそうな糸口がつかめたところで挫折 :(

SocketReaderPatchApacheLogPatch とQuartzComposerのカスタムパッチをつくってきたのですが、ここらでRubyで書くかと思い立ちました。 バベル案内ホワイの(感動的)Rubyガイド を読んで、こんなにもRubyが世界に受け入れらているのかと感動したことと、いくつかCustomPatchをつくってみて、CustomPatchj作成の概観がつかめてきたから。

どうやるか

RubyCocoaのsvn trunkを使います。 最近のリビジョンでは、QuartzComposer.frameworkが使えるようになっているのです。

>> require 'osx/cocoa'
r=> true
>> OSX.require_framework 'QuartzComposer'
=> true
>> OSX::QCStructure
=> OSX::QCStructure
>> OSX::QCPatch
=> OSX::QCPatch

ひゃっほう。

で、いつもどおりQuartzComposer CustomPatch の Xcode template をつかって、Xcodeプロジェクトをはじめます。

RubyCocoa.frameworkをリンクしておき、RubyPatchPrincipla.m を

#import <rubyCocoa/RubyCocoa.h>
#imporut "RubyPatchPrincipal.h"
 
@implementation RubyPatchPlugin
+ (void)registerNodesWithManager:(GFNodeManager*)fp8
{
  static bool loaded = false;
  if (!loaded) {
    if (RBBundleInit("/tmp/custom_patch.rb", self, nil)) {
      NSLog(@"[RubyPatchPlugin.registerNodesWithManager] RBBundleInit failed");
    } else {
      loaded = true;
    }
  }
 
  Class helperClass = NSClassFromString(@"RubyPatch");
  [fp8 registerNodeWithClass:helperClass];
}
@end

のように書きました。 RubyCocoaのbundle生成用APIを用いています。

loadされるRubyスクリプトはこういうもの。

require 'osx/cocoa'
OSX.require_framework 'QuartzComposer'
 
class RubyPatch < OSX::QCPatch
  #attr_accessor :inputFoo, :outputBar
 
  def RubyPatch.logger=(l)
    @@logger = l
  end
 
  def RubyPatch.executionMode
    @@logger.info("executionMode")
    2
  end
 
  def RubyPatch.allowsSubpatches
    @@logger.info("allowsSubpatches")
    0 # in objc.h, NO is defined as 0
  end
 
  def RubyPatch.timeMode
    @@logger.info("timeMode")
    1
  end
 
 
  def init
    @@logger.info("init")
    @inputFoo = OSX::QCBooleanPort.alloc.init
  end
 
  def initWithIdentifier(fp8)
    @@logger.info("initWithIdentifier")
    init
    self
  end
 
  def setup(fp8)
    @@logger.info("setup %s, %s", fp8, fp8.class)
    fp8
  end
 
  def cleanup(fp8)
    @@logger.info("cleanup")
  end
 
  def enable(fp8)
    @@logger.info("enable")
  end
 
  def disable(fp8)
    @@logger.info("disable")
  end
 
  def execute(fp8, time, arg)
    @@logger.info("execute")
    true;
  end
end
 
OSX.init_for_bundle do |bundle, param, logger|
logger.info("init bundle=%s param=%s", bundle, param)
  RubyPatch.logger = logger
end

templateが生成するObjective-Cのコードを、Rubyに書き下したものですね。

実行

ビルドもすんなり通り、さっそく実行してみます。

qc-exception-rubycocoapatch.png

アウチ。 なんか例外が投げられてしまいます。 出しているログを眺めてみると…

正常なもの (Objective-Cで書いたカスタムパッチ)

2007-07-14 14:35:03.568 Quartz Composer[15580] RubyPatch.plugin (Quartz Composer): init bundle=NSBundle  (loaded) param=
2007-07-14 14:35:03.708 Quartz Composer[15580] initWithIdentifier
2007-07-14 14:35:03.708 Quartz Composer[15580] executionMode
2007-07-14 14:35:03.708 Quartz Composer[15580] timeMode
2007-07-14 14:35:03.710 Quartz Composer[15580] allowsSubpatches
2007-07-14 14:35:03.741 Quartz Composer[15580] allowsSubpatches
2007-07-14 14:35:03.741 Quartz Composer[15580] allowsSubpatches
2007-07-14 14:35:03.741 Quartz Composer[15580] allowsSubpatches
2007-07-14 14:35:03.785 Quartz Composer[15580] setup

だめなもの (RubyCocoaで書いた)

2007-07-14 14:36:11.300 Quartz Composer[15602] RubyPatch.plugin (Quartz Composer): init bundle=NSBundle  (loaded) param=
2007-07-14 14:36:11.301 Quartz Composer[15602] assert passed !
2007-07-14 14:36:11.342 Quartz Composer[15602] RubyPatch.plugin (Quartz Composer): initWithIdentifier
2007-07-14 14:36:11.342 Quartz Composer[15602] RubyPatch.plugin (Quartz Composer): init
2007-07-14 14:36:11.345 Quartz Composer[15602] RubyPatch.plugin (Quartz Composer): allowsSubpatches
2007-07-14 14:36:11.377 Quartz Composer[15602] RubyPatch.plugin (Quartz Composer): allowsSubpatches
2007-07-14 14:36:11.378 Quartz Composer[15602] RubyPatch.plugin (Quartz Composer): allowsSubpatches

よく見てみると、RubyCocoaで書いた方は、initWithIdentifier のあとに executionMode, timeMode が呼ばれることなく allowsSubpatches が呼ばれています。 executionMode, timeMode が分からないので、QuartzComposer としてはこのパッチをどの種類にして良いか分からず、レンダリングできませんよーという例外がきてるっぽい。

問題

ではなぜ executionMode, timeMode が呼ばれないか、考えてみます。

QCPatchのヘッダによると、executionMode, timeMode はそれぞれ int を返すメソッドのようです。 ところが、RubyCocoaで用意した executionMode, timeMode は返り値の型を指定していません。 ちょっと調べたところ、RubyCocoaではRubyのメソッドが整数型を返すとき、Objective-Cの世界にはNSDecimalNumberの型を返すようになっているようです。

その結果、executionMode, timeMode は別のメソッドとして認識されてしまい、QCPatchのメソッドをオーバーライドできず、呼ばれていないのではないかと推測しました。

解決案

  1. intで返すようにRubyCocoaでなんとかする
  2. QCPatchをRubyクラスで継承せずにObjective-Cのクラスで継承させ、そのメンバにRubyクラスのdelegateのオブジェクトをもたせて、実際の処理はdelegateオブジェクトに振る

2が手っ取り早そうですが、いまひとつかっこいくないなぁ。

別の問題

CustomPatchのテンプレートによると、QCPatchを継承したクラスのメンバに QCPortクラスのインスタンス (ex. inputEnable, outputString など) を書くことで、パッチのinput/outputポートを指定できるような仕組みになっています。

じゃあ、

attr_accessor :inputEnable, :outputString
...

みたいにすればいいのか、というとどうもそうでもないみたいで…

やはり解決案2を使うしかないのかもです。

ApacheLogPatch 0.1

ApacheLogPatch というものをつくりました。 Apacheのログを、Quartz Composer で派手に tail -f するというものです。 パス、リモートホストのアドレス、リファラー、検索キーワードが表示されます。

Apacheの設定ファイルを書き換えたりと動くようにするまでの敷居が妙に高いわりには、動いたときの「おー、ふーん」感がなんともいえませんが、動かしてみようという方はどうぞぜひ。

自宅サーバをMacで動かしている方という、たいへん狭いターゲットを狙いました。 スクリーンセーバーとして動かしておき、自分のサーバに人がどんな検索キーワードできているのか、なんてことを夕食どきにご飯を食べながら眺めてみるのも一興ではないでしょうか。

ビデオとスクリーンショット

ビデオは、本当はDVカメラで撮影したちゃんとしたものがあったのですが、どうもFirewireからの取り込みができず挫折して vnc2swf を使いました。ので、フレームレートが悲惨なことになっており、本当にこれ、動かして楽しいの? みたいなことになってます。楽しいんですよ。

Python版のvnc2swfで録り直しました。だいぶ見れるレベルになったのでは。

QuartzComposer の CustomPatch で Signal を生成する方法

QuartzComposer の Patch には、Signal という 一瞬だけ True になったあとまたすぐ False になるような Port をもつものがあります。たとえば、StopWatch とか、Counterとか。

では、どのようにして CustomPatch で Signal を生成すればよいか、いろいろ試行錯誤してみたので、メモを残しておきます。

Port

@interface SomePatch : QCPatch {
  QCBooleanPort *outputSignal;
  ....
}

のように、Boolean な Port を宣言しておきます。

Configuration

まず、CustomPatch の timeMode を 1 にして、常に Patch が実行されるようにしておきます。

+ (int)timeMode { return 1; }

としておけばよいでしょう。

ここらへんの詳細については、QCPatch Configuration に書かれてあります。

Signal を発生させる

1〜2回、execute が実行される間に outputSignal の値が True になっていれば、Signal が発生したとみなされるようです。

- (BOOL)execute:(id)fp8 time:(double)fp12 arguments:(id)fp20 {
  static int count = 0;
 
  // signal hack
  if (TRUE == [outputSignal booleanValue]) {
    if (1 == count++) {
      [outputSignal setBooleanValue:FALSE];
      count = 0;
    }
  }
 
  return YES;
}

みたいにしました。[outputSignal setBooleanValue:TRUE] にするのは、どこか別のスレッドにてやっています。

まとめ

俺Patchで Signal が送れるようになれば、データが変わったタイミングを後段のPatchに伝えることができて、なにかと使えそうです。RSS Feed Patch や、Image Downloader も、FeedやImageのダウンロードが終わったタイミングをSignalで通知しているわけで。

おまけ

QuartzComposer の CustomPatch をデバッグするには、NSLog が便利です。 というか、つくった Patch は plugin 形式なので、いつものようにデバッガを直接起動することができないので。

NSLogのログは、 /Library/Logs/Console/”自分の uid”/console.log に出力されるので、tail -f などとして眺めるとグッドです。

QuartzComposer の CustomPatch をつくるための Xcode template

の0.2が公開されてました

なにが素晴らしいかって、

  • より多くの種類の Port ( Image, Structure, …)
  • ビルドしたら、自動的に /Library/Graphics/Patch につくったPatchがインストールされる

というところ。

以前のもの よりも格段に使えるものになっているので、QuartzComposerで俺パッチをつくるぜ! という方はぜひインストールしてみればよいと思います。

IRCのメッセージをQuartzComposerに表示

SocketReaderPatchの応用例として、IRCで流れるメッセージを、QuartzComposerに表示させてみました。

qcbot

ScreenCast (QuickTime)

ダウンロード

qc.rb

インストール

  • rbot を Rubygem などでインストールする
  • インストールしたrbotのディレクトリ (Rubygemの場合は /opt/local/lib/ruby/gems/1.8/gems/rbot-0.9.10) の /data/rbot/plugins/ に、qc.rb をコピーする

つかいかた

  • SocketReaderPatch を使うQuartzComposerのCompositionを開いておく (サーバ側のSocketをlistenしておく)
  • 適当なサーバ、チャンネルにrbotを常駐させる (rbotのマニュアルを参照)

これで、QuartzComposerにIRCのログが表示されるようになります。

コード

たったこれだけ。

    require 'kconv'
 
    class QCPlugin < Plugin
      PORT = 12345
 
      def initialize
        super
        @sock = TCPSocket.new('localhost', PORT)
      end
 
      def listen(m)
        return unless m.kind_of?(PrivMessage)
 
        @sock.print Kconv.toutf8(m.message)
      end
 
      def help(plugin, topic="")
        "QuartzCompositor from IRC log"
      end
 
      def privmsg(m)
        #puts m.message
      end
    end
    plugin = QCPlugin.new
    plugin.register("qc")

その他

元々は、Ruby会議2日目に思いついたネタだったので、かれこれ1週間たってしまいました。長かった。 RubyCococaでやろうとか、dRubyだとか何かと野心的にアプローチをとってみたのですが、けっきょくはSocketでごりごり書くというオーソドックスなスタイルに。

それにしても、SocketReaderPatchをつくるときにもqc.rbのときにも、オブジェクト指向の生産性の高さを実感しました。 既存の振舞いをほんの少しカスタマイズしたいときの継承の威力というか。

SocketReaderPatch 0.1

SocketReaderPatch

Mac OSX の Quartz Composer で、Socketから文字列を読み込んで後段に渡すGeneratorパッチ

を書きました。

晴れた土曜の勢いで書いたのでなにかとバグがあるかもしれませんが、プロトタイプを出すことがたいせつなので公開します。フィードバックいただけるとたいへん喜びます。

2007.06/16 20:34 追記

英語の説明文も追加。QuartzComposer CustomPatch Xcode template 配布サイトに感謝のコメントを残しておいた。

2007.06/16 22:50 追記

ScreenCast を追加しました。

ScreenCastには、 Making Free Screencasts on OS X を参考にして、vnc2swfを使いました。マウスカーソルはでないのだけど、フリーでScreenCastがつくれるのは著しく便利です。

swf → mpeg4 には、QuickTime Proをつかって。なぜかH.264ではエンコードできなかったのでMPEG-4。

RubyCocoaでQuartzComposerパッチ

という小プロジェクトを今週はやっています。

RubyCocoa, CocoaProgramming, Ruby, QuartzComposer, XCode, dRuby といった様々なテクノロジーをいっきに学んでいて、かなりお腹いっぱいになってきます。

* IRCのメッセージをrbotで拾ってdRubyで投げる
* サービス側のdRubyでメッセージを受け取る
* RubyCocoaでbundleを書く
* QuartzComposerのbundleインターフェイスから、RubyCocoaでつくったクラスを指定する方法が分からなくてめげる ← いまここ

フレームワークは便利なんだけれど、なんというか全体が見えなくてもどかしく、できないことが積もっていくうちにめげ気分になるのが難点です。僕だけ?

Home > Tags > 206

Feeds

Return to page top