Clojure で棒読みちゃんクライアント再び

以前実験的に作りましたが、あれから色々コードの書きかたを憶えたので、もうすこしちゃんと書いてみました。アドバイスなどありましたら是非。

棒読みちゃんクライアントプログラム

(ns bouyomi-client.core
  (:gen-class))

(defmacro create-ctor
  "デフォルト値付き record コンストラクタ生成 helper
     ctor-name : 生成するコンストラクタ関数の名前
     default   : デフォルト値として使用する record インスタンス"
  [ctor-name default]
  `(defn ~ctor-name
     ([] ~default)
     ([k# v# & kvs#]
        (apply assoc ~default (concat [k# v#] kvs#)))))

;; サーバー情報
(defrecord ServerInfo
  [#^String  host  ; IPアドレス or ホスト名
   #^Integer port  ; ポート番号。棒読みちゃんデフォルトは 50001。
   ])
(def default-server-info (ServerInfo. "127.0.0.1" 50001))
(create-ctor new-server-info default-server-info)

;; 送信データヘッダ
(defrecord DataHeader
  [#^Short   command  ; [コマンド] 0x0001 (詳細は棒読みちゃん付属の Sample Source 参照)
   #^Short   speed    ; [速度] -1:デフォルト, 50~300
   #^Short   pitch    ; [音程] -1:デフォルト, 50~200
   #^Short   volume   ; [音量] -1:デフォルト,  0~100
   #^Short   voice    ; [声質]  0:デフォルト,  1~8:AquesTalk, 10001~:SAPI5
   #^Byte    coding   ; [文字コード] 0:UTF-8, 1:Unicode, 2:Shift-JIS
   #^Integer datalen  ; [送信文字列のバイト長]
   ])
(def default-data-header (DataHeader. 1 -1 -1 -1 0 0 0))
(create-ctor new-data-header default-data-header)

;; byte array 生成メソッド群
(defmulti into-byte-array
  (fn [obj & param] (class obj)))

(defmethod into-byte-array DataHeader
  [this]
  (.array (doto (java.nio.ByteBuffer/allocate (+ (-> Short/SIZE   (/ ,,, 8) (* 5))
                                                 (-> Byte/SIZE    (/ ,,, 8))
                                                 (-> Integer/SIZE (/ ,,, 8))))
            (.order java.nio.ByteOrder/LITTLE_ENDIAN)
            (.putShort (:command this))
            (.putShort (:speed this))
            (.putShort (:pitch this))
            (.putShort (:volume this))
            (.putShort (:voice this))
            (.put (.byteValue (:coding this)))
            (.putInt (:datalen this)))))

(defmethod into-byte-array String
  ([this] (.getBytes this))
  ([this coding] (.getBytes this coding)))

(defn make-send-data
  "socket への送信データ作成
     msg : 送信する文字列
     kvs : DataHeader のパラメータ変更用オプション
   example
     (make-send-data \"あいうえお\")
     (make-send-data \"あいうえお\" :speed 100 :voice 7)"
  [msg & kvs]
  (let [arr-msg (into-byte-array msg)
        arr-msg-len (count arr-msg)
        arr-head (into-byte-array (->> (concat [:datalen arr-msg-len] kvs)
                                       (apply new-data-header)))]

    (.array (doto (java.nio.ByteBuffer/allocate (+ arr-msg-len
                                                   (count arr-head)))
              (.put arr-head)
              (.put arr-msg)))))
                    
(defn send-msg
  "棒読みちゃんへメッセージ送信
     srv : 送信先サーバー
     msg : 送信する文字列
     kvs : DataHeader のパラメータ変更用オプション (make-send-data の example 参照)"
  [srv msg & kvs]
  (with-open [sock (java.net.Socket. (:host srv)
                                     (:port srv))
              os (.getOutputStream sock)]
    (.write os (apply make-send-data msg kvs))))

以前のプログラムでは、ヘッダとメッセージを別々に Socket Stream へ書き込んでいましたが、今回はひとつの配列にまとめて一気に送っています。

■使い方

(1) new-server-info でサーバー情報を作成。
(2) send-msg で送信。
の2ステップです。どちらの関数もオプション引数で挙動を変更できるようになっています。
なお、棒読みちゃんはあらかじめ起動しておいてください。

■使用例

ローカルホストで起動している棒読みちゃんへ送信

(let [srv (new-server-info)]
  (send-msg srv "僕と契約して、魔法少女になってよ"))

棒読みちゃんが起動しているPCのアドレスと、声質、スピードを指定。さらにメッセージを複数連続で送信。

(let [srv (new-server-info :host "192.168.1.10")
      snd #(send-msg srv % :voice 2 :speed 70)]
  (map snd ["いいぜ"
            "てめえが何でも思い通りに出来るってなら"
            "まずはそのふざけた幻想をぶち殺す!!"]))

単語のマップをあたえると、まとめて教育してくれる関数

(defn study-words
  [m]
  (let [srv (new-server-info)
        snd #(send-msg srv % :speed 300 :pitch 200)
        study #(snd (apply format "教育(%s=%s)" %))]
    (map study m)))

教育してみる。

(study-words {"clojure" "くろーじゃー"
              "python" "ぱいそん"
              "c++" "しーぷらぷら"
              "haskell" "はすける"
              "ocaml" "おきゃむる"})

教育コマンドを有効にするには、棒読みちゃんを配信者モードにしておく必要あります。

以上