ポケット・ミクをJava/Clojureから操作してみるの続きです。
Clojureで関数化してみました。
ビブラートはShortMessageでCONTROL_CHANGEを指定すればできます。ビブラートの大きさも指定できます。
ピッチベンドはShortMessageでPITCH_BENDを指定すればできるようです。
(ns pokemiku.core
(:import [javax.sound.midi MidiSystem ShortMessage SysexMessage MidiDevice Receiver]
[javax.xml.bind DatatypeConverter]))
(defn ^MidiDevice get-device []
(some->> (seq (MidiSystem/getMidiDeviceInfo))
(filter #(re-find #"NSX-39" (:name (bean %))))
(filter #(re-find #"External" (:description (bean %))))
first
(MidiSystem/getMidiDevice)))
(defn note [^Receiver r data1 data2]
(.send r (ShortMessage. ShortMessage/NOTE_ON data1 data2) -1))
(defn note-off [^Receiver r data1 data2]
(.send r (ShortMessage. ShortMessage/NOTE_OFF data1 data2) -1))
(defn vibrato
([^Receiver r] (vibrato r 127))
([^Receiver r vol] (.send r (ShortMessage. ShortMessage/CONTROL_CHANGE 1 vol) -1)))
(defn vibrato-off [^Receiver r] (vibrato r 0))
(defn pitch-bend [^Receiver r n]
(when (<= -64 n 63)
(.send r (ShortMessage. ShortMessage/PITCH_BEND 1 (+ 64 n)) -1)))
(defn pitch-bend-off [^Receiver r] (pitch-bend r 0))
(defn- small-char? [x] (re-find #"[ぁぃぅぇぉゃゅょ]" (str x)))
(def ^:private base-chars "あいうえおかきくけこがぎぐげごきゃきゅきょぎゃぎゅぎょさすぃすせそざずぃずぜぞしゃししゅしぇしょじゃじじゅじぇじょたてぃとぅてとだでぃどぅでどてゅでゅちゃちちゅちぇちょつぁつぃつつぇつぉなにぬねのにゃにゅにょはひふへほばびぶべぼぱぴぷぺぽひゃひゅひょびゃびゅびょぴゃぴゅぴょふぁふぃふゅふぇふぉまみむめもみゃみゅみょやゆよらりるれろりゃりゅりょわうぃうぇうぉんmNJn")
(defn split-chars [s]
(let [s (str (apply str s) " ")]
(->> (map #(cond (small-char? %2) (str %1 %2) (not (small-char? %1)) (str %1)) s (drop 1 s))
(filter some?))))
(def ^:private char-map
(->> (zipmap (split-chars base-chars) (range))
(merge {"づぁ" 26 "づぃ" 27 "づ" 28 "づぇ" 29 "づぉ" 30 "ゐ" 120 "ゑ" 121 "を" 122 "N\\" 123 "ぢ" 37 "ヴ" 78})))
(def ^:private bin-map
(->> (mapcat (fn [[k v]] [(keyword k) (DatatypeConverter/parseHexBinary (format "F0437909110A00%02XF7" v))]) char-map)
(apply hash-map)))
(defn set-char [^Receiver r ch]
(when-let [data (bin-map (if (char? ch) (keyword (str ch)) (keyword ch)))]
(.send r (SysexMessage. data (count data)) -1)))
さらにこれらを利用するplay
関数を作りました。
play
関数にはテンポとともに4つずつ[文字、音の高さ、音の長さ、音の大きさ]を指定していきます。
Java/Clojureで固定時間で繰り返し実行するにはThread.sleepを使うよりもExecutors.newScheduledThreadPool().scheduleAtFixedRate()を使ったほうがより正確な間隔で繰り返してくれます。
以下では1/120秒ごとに繰り返し実行しています。
また、文字を指定した直後のノート(音符)ではその文字が反映されないことがたまにあるので、ノートの直後に次のノートの文字指定をしています。
(ns pokemiku.util
(:require [pokemiku.core :refer :all])
(:import [java.util.concurrent Executors ScheduledExecutorService ScheduledFuture TimeUnit]
[javax.sound.midi ShortMessage]))
(def base-octave [60 62 64 65 67 69 71])
(defn play [tempo & notes]
(let [n (partition 4 notes)
base 120
tempo (atom tempo)
n (atom n)
sum (atom 0)
pool (Executors/newScheduledThreadPool 1)
f #(if (nil? @n) (.shutdown pool)
;; char pitch length volume
(let [[c p l v] (first @n)]
(if (zero? @sum)
(condp = c
:V (vibrato % v)
:B (pitch-bend % v)
:T (reset! tempo v)
:R (note-off % p v)
(do (set-char % c) (note % p v)))
(when (< @sum 2) (set-char % (first (second @n))))) ; set-charを即座に反映するため
(swap! sum inc)
(when (<= (* base l (/ 60 @tempo)) @sum)
(vibrato-off %) (pitch-bend-off %) (reset! sum 0) (swap! n next))))]
(with-open [dev (get-device)]
(.open dev)
(let [r (.getReceiver dev) rate (Math/round (double (/ 1000000 base)))]
(doto pool
(.scheduleAtFixedRate #(f r) 0 rate TimeUnit/MICROSECONDS)
(.awaitTermination 1 TimeUnit/DAYS))))))
簡単な使い方の例は以下のとおりです。
以下ではテンポ(1分間に四分音符が入る数)が60で、初めは「ど」の文字で72の高さで四分音符1つ分の長さで、音の大きさ127で発声します。
:Vや:Bなどのパラメータはおよそ以下のとおりです。
- :V ビブラート [未使用、長さ、大きさ]
- :B ピッチベンド [未使用、長さ、変化量]
- :R 休符 [OFFにする音の高さ、長さ、速度]
- :T テンポ変更 [未使用、未使用、テンポ]
(play 60 :ど 72 1 127 :V 1 1 127 :R 72 1/2 0 :れ 74 1/2 127 :B 1 1/5 2 :み 76 1 127 :T 0 0 120 :ど 72 1 127 :V 1 1 127 :れ 74 1/2 127 :B 1 1/5 2 :み 76 1 127)
よりプログラムっぽくすると以下のような感じでも操作できます。
(apply play 150 (interleave
(split-chars (repeat 3 "どれみふぁそらし"))
(concat (map #(- % 12) base-octave) base-octave (map #(+ 12 %) base-octave))
(repeat 1) (repeat 127)))
(play 150 :ど 84 1/5 127 :V 1 2 127)
(apply play 120
(interleave (cycle "みく")
(let [c [76 77 74 72]] (concat c (map inc c) (reverse c) (map inc (reverse c) )))
(cycle [1/2 1/3 1 2/3]) (repeat 127)))
まだまだ他にもいろいろできるみたいですね。