javax.sound.midi を使って MIDI を鳴らします
もう少し正確に言うと, OS の方で設定されている MIDI 音源に MIDI メッセージを送ります.
適当な MIDI ファイル (拡張子 .mid) を探してきて, ダブルクリックで鳴るようようなら, 鳴るでしょう.
デフォルトのシンセサイザーで, チャンネル 0 番に, 音色デフォルト (たぶんピアノになっているでしょう) で,
C4 (ノート番号 48) の音を, ベロシティ 64 で, 1000ミリ秒鳴らすには,
(with-open [synth (doto (javax.sound.midi.MidiSystem/getSynthesizer) .open)]
(let [channel (aget (.getChannels synth) 0)]
(.noteOn channel 48 64)
(Thread/sleep 1000)
(.noteOff channel 48 64)))
とします.
MIDI メッセージは, 元々は楽器の操作を転送するものなので, 「何秒間鳴らす」というメッセージではなく,
「打鍵した」というメッセージ (note on) と「離鍵した」というメッセージ (note off) を,
適当なタイミングで送ることで, 音を鳴らしたり止めたりします.
ノート番号は 0~127 で半音単位で指定します. 48 が C4 (真ん中のド) です.
どんな音律で演奏するかは, 音源側に委ねられますが, 普通は何も設定しなければ 12平均律になっているでしょう.
ベロシティは「速度」という意味ですが, これも, 打鍵の速さを情報として送るところから来ています.
大抵の音源は, この打鍵速度の情報に基づいて音量を決めますので, さしあたり音量と考えても良いと思います.
0~127 で指定します.
もう少し高レイヤーが好みの方は, 便利なライブラリが沢山ありますので, そちらをどうぞ.
「俺が MIDI ソフトを作る」という低レイヤーの好きな方はいろいろ試してみてください.
以上です. というのもなんなのでもう少し.
竹内関数の引数の変遷を遅延シーケンスに
LISPer 大好き竹内関数です.
(defn tarai [x y z]
(if (<= x y) y
(tarai (tarai (dec x) y z)
(tarai (dec y) z x)
(tarai (dec z) x y))))
説明要らないですね.
さて, この竹内関数が再帰的に呼び出されている時の引数を, たらい回されている最中に知りたいとします.
(defn tarai [x y z]
(println x y z)
(if (<= x y) y
(tarai (tarai (dec x) y z)
(tarai (dec y) z x)
(tarai (dec z) x y))))
user=> (tarai 2 1 0)
2 1 0
1 1 0
0 0 2
-1 2 1
1 0 2
0 0 2
-1 2 1
1 1 0
0 2 1
2
見る事はできますが, 使えないですね.
ちょっと手を加えて, 左の引数から順に正格評価されると仮定した場合に,
この関数が呼ばれた時の引数を遅延シーケンスで得られるようにしましょう.
(defn args-and-result [x y z]
(if (<= x y) [[[x y z]] y]
(let [[a0 r0] (args-and-result (dec x) y z)
[a1 r1] (args-and-result (dec y) z x)
[a2 r2] (args-and-result (dec z) x y)
[a3 r3] (args-and-result r0 r1 r2)]
[(cons [x y z] (lazy-seq (concat a0 a1 a2 a3))) r3])))
(defn args [x y z] (first (args-and-result x y z)))
args 関数に竹内関数の引数を与えると, 3つの引数をベクタにしたものの遅延シーケンスが得られます.
user=> (args 2 1 0)
([2 1 0] [1 1 0] [0 0 2] [-1 2 1] [1 0 2] [0 0 2] [-1 2 1] [1 1 0] [0 2 1])
また
(defn view-args [x y z t]
(doseq [xyz (args x y z)]
(println xyz)
(Thread/sleep t)))
などとすれば t ミリ秒毎に, 引数の変遷を見る事ができます.
user=> (view-args 2 1 0 500)
[2 1 0]
[1 1 0]
[0 0 2]
[-1 2 1]
[1 0 2]
[0 0 2]
[-1 2 1]
[1 1 0]
[0 2 1]
nil
竹内関数で音楽生成
さて, 竹内関数で音楽生成 という素敵なエントリをご存知でしょうか?
もしご存知でなければ, 是非ご覧に(お聞きに)なった上で, ここに戻ってきてください.
aike さんによる音楽生成を簡易に再現してみましょう.
ここでは, 和音を順番に鳴らすにとどめ, 自動アルペジオ機能はみなさんへの課題とします.
; 演奏中に音名も表示したいので,
(defn nn2name [nn]
({0 "c" 1 "cis" 2 "d" 3 "es" 4 "e" 5 "f"
6 "fis" 7 "g" 8 "as" 9 "a" 10 "b" 11 "h"}
(mod nn 12)))
; たらい回しの各回で, 引数を表示し且つ, arg2nn で指定された「引数 -> ノート番号」の対応に従い,
; t ミリ秒間, 和音を鳴らします.
(defn listen-args [x y z arg2nn t]
(with-open [synth (doto (javax.sound.midi.MidiSystem/getSynthesizer) .open)]
(let [channel (aget (.getChannels synth) 0)]
(doseq [xyz (args x y z)]
(println (apply str
(mapcat #(let [nn (arg2nn %)]
[% " -> (" (nn2name nn) "[" nn "])\t"]) xyz)))
(dorun (map #(.noteOn channel (arg2nn %) 64) xyz))
(Thread/sleep t)
(dorun (map #(.noteOff channel (arg2nn %) 64) xyz))))))
; aike さんの割当: 0 を E4 として, ハ長調のダイアトニックトーンに割り当てます.
(def e-ph
{-2 48 -1 50 0 52 1 53 2 55 3 57 4 59 5 60 6 62 7 64 8 65 9 67 10 69})
; 別の割当も試してみましょう.
(def c-io
{-2 45 -1 47 0 48 1 50 2 52 3 53 4 55 5 57 6 59 7 60 8 62 9 64 10 65})
user=> (listen-args 2 1 0 c-io 1200)
2 -> (e[52]) 1 -> (d[50]) 0 -> (c[48])
1 -> (d[50]) 1 -> (d[50]) 0 -> (c[48])
0 -> (c[48]) 0 -> (c[48]) 2 -> (e[52])
-1 -> (h[47]) 2 -> (e[52]) 1 -> (d[50])
1 -> (d[50]) 0 -> (c[48]) 2 -> (e[52])
0 -> (c[48]) 0 -> (c[48]) 2 -> (e[52])
-1 -> (h[47]) 2 -> (e[52]) 1 -> (d[50])
1 -> (d[50]) 1 -> (d[50]) 0 -> (c[48])
0 -> (c[48]) 2 -> (e[52]) 1 -> (d[50])
nil
音名で表示されている和音が演奏されましたか?
user=> (listen-args 10 5 0 e-ph 1200)
10 -> (a[69]) 5 -> (c[60]) 0 -> (e[52])
9 -> (g[67]) 5 -> (c[60]) 0 -> (e[52])
8 -> (f[65]) 5 -> (c[60]) 0 -> (e[52])
7 -> (e[64]) 5 -> (c[60]) 0 -> (e[52])
6 -> (d[62]) 5 -> (c[60]) 0 -> (e[52])
5 -> (c[60]) 5 -> (c[60]) 0 -> (e[52])
4 -> (h[59]) 0 -> (e[52]) 6 -> (d[62])
3 -> (a[57]) 0 -> (e[52]) 6 -> (d[62])
2 -> (g[55]) 0 -> (e[52]) 6 -> (d[62])
...
とすると aike さんの動画と同じ和音の変遷が聞けます. 343,073 小節あるので気をつけてください.
クリスマス関係なくてすみません.
core.async も使ってなくてすみません.