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

ごちゃログぴこっ

はなまるデジタル創作紀行(DTM、TAS、いろいろな技術)

各種MIDI2MMLプログラムを比較してみた

このたび、ごちゃと loveemu さんで PetiteMM という変換ツールを発表しました。開発に着手する前「既に優れたツールがあれば開発する必要はないのでは?」と思ったのですが、いくつか探して試した限りでは納得行くものが見つかりませんでした。

そんなわけで、この記事ではサンプルファイル mmldec.mid を各種MIDIコンバータにかけて難癖をつけながら、PetiteMM のステマを行っていきたいと思います。

PetiteMM (2013-09-02)

t128r1
/* 最も変換しやすそうな旋律(ベタ打ち) */
o5c8d8e8f8g4a8f8
e8r8d8r8c4.r8
/* Gate ×0.85 */
c16.r32d16.r32e16.r32f16.r32g8..r32a16.r32f16.r32
e16.r8r32d16.r8r32c3r6
/* Gate -4 */
c16.r32d16.r32e16.r32f16.r32g8..r32a16.r32f16.r32
e16.r8r32d16.r8r32c4.r8
/* 途中で長い休符を挟む */
g8.f16e16r16g16r1
r1
r1
r16
f16r16e16r16d8.r16
/* ベロシティ変更 */
g8.f16e16r16g16r16f16r16e16r16d8.r16
d1
/* 三連符、クオンタイズ変更、休符も入れる */
c6d6e6f16r48g16r48a16r48f16r12r48f16r48
/* 長い音符 */
e16e16e16e16d16r16d16r16c1&c1&c4
r4
/* 拍子変更(試しに小節の区切りをまたぐ) */
c8d8e8f8g8a4
f8e16.r32e16.r32d16.r32d16.r32
c2r4
/* 4分音符±1tick */
c4r4c4r4
/* 付点4分音符±1tick */
e4.r8e4.r8
/* 複付点4分音符±1tick */
g4..r16g4..r16
/* クオンタイズを伴う「中途半端なトラック終了」 */
c16d16e16f16g8a16f16e24r12d24r12c48&c64

変換結果を再生できるようにニコニコ大百科にも置きました。タイを ^ から & に置換してコメントを記入しましたが、それ以外はデフォルト設定で変換した通りの出力です。

クオンタイズのための r32 が少々邪魔に感じられますが、精度を16分音符単位に落とせば取り除くことも可能です。

  • MMLの内容のシンプルさを追求しつつ、クオンタイズを極力損なわないよう休符を挿入します。
    • シンプルさを犠牲にしてタイミングを正確に出力できるオプションも備えています。
  • 仕組み上、連符や短い音符によってトラック間でずれが広がっていくようなことは絶対に起こりません。
  • 細かすぎる音符や付点の数を制御する仕組みがあり、ターゲットの制約に合わせた変換が可能です。
  • SMFフォーマット1で単一チャンネルの演奏を複数トラックに分けている場合、個別トラックで出力します。
  • 拍子情報にもとづいて、小節ごとに改行します。(ただしタイでつながっている音符は改行しない*1
  • tick単位による出力 c%24 などは一切出現しません。
  • 和音のあるトラックは単音のみが変換されます。

改善できそうな点もありますが、なかなか良さそうではないでしょうか! (*>ヮ<*)

開発着手時は「きっと既存のツールだって同等以上の性能があるだろう」と思って探したのですが……。

*1:タイで改行しないのは特別に意図があってそうしたわけでもないです。半ば開発上の経緯的な問題です。

続きを読む

FlMMLでPCエンジンのノイズを再現したいと思ったけど

FlMMLPCエンジンのノイズを再現したいと思ったので、周波数からGB音源やFC音源に近似させてみたいと思いました。

REG FRQ(PCE) FRQ(GB) FlMML
0 1804.21 1638.40 o2c+
1 1864.35 2048.00 o2c
2 1928.63 2048.00 o2c
3 1997.51 2048.00 o2c
4 2071.50 2048.00 o2c
5 2151.17 2048.00 o2c
6 2237.22 2340.57 o1b
7 2330.43 2340.57 o1b
8 2431.76 2340.57 o1b
9 2542.29 2730.67 o1a+
10 2663.35 2730.67 o1a+
11 2796.52 2730.67 o1a+
12 2943.71 2730.67 o1a+
13 3107.24 3276.80 o1a
14 3290.02 3276.80 o1a
15 3495.65 3276.80 o1a
16 3728.69 4096.00 o1g+
17 3995.03 4096.00 o1g+
18 4302.34 4096.00 o1g+
19 4660.87 4681.14 o1g
20 5084.58 5461.33 o1f+
21 5593.04 5461.33 o1f+
22 6214.49 6553.60 o1f
23 6991.30 6553.60 o1f
24 7990.06 8192.00 o1e
25 9321.73 9362.29 o1d+
26 11186.08 10922.67 o1d
27 13982.60 13107.20 o1c+
28 18643.47 18724.57 o0b
29 27965.20 26214.40 o0a
30 55930.40 52428.80 o0f
31 111860.80 131072.00 o0d

あれれー。

REG FRQ(PCE) FRQ(NES) FlMML
0 1804.21 1759.85 o1d
1 1864.35 1759.85 o1d
2 1928.63 1759.85 o1d
3 1997.51 1759.85 o1d
4 2071.50 1759.85 o1d
5 2151.17 1759.85 o1d
6 2237.22 1759.85 o1d
7 2330.43 1759.85 o1d
8 2431.76 1759.85 o1d
9 2542.29 1759.85 o1d
10 2663.35 3523.17 o1c+
11 2796.52 3523.17 o1c+
12 2943.71 3523.17 o1c+
13 3107.24 3523.17 o1c+
14 3290.02 3523.17 o1c+
15 3495.65 3523.17 o1c+
16 3728.69 3523.17 o1c+
17 3995.03 3523.17 o1c+
18 4302.34 4697.57 o1c
19 4660.87 4697.57 o1c
20 5084.58 4697.57 o1c
21 5593.04 4697.57 o1c
22 6214.49 7046.35 o0b
23 6991.30 7046.35 o0b
24 7990.06 7046.35 o0b
25 9321.73 9419.86 o0a+
26 11186.08 9419.86 o0a+
27 13982.60 14092.70 o0a
28 18643.47 17720.52 o0g+
29 27965.20 27965.20 o0f+
30 55930.40 55930.39 o0e
31 111860.80 111860.78 o0d+

結構かぶっちゃうんですね。

@4 のノイズはあれはあれで、乱数ステップのタイミングが線形周期じゃないみたいに見えるので、@N の範囲が128段階とはいえ複雑です。

最初鳴らし方を間違えていて「全然違う」と思ったのですが、正しく鳴らしてみたらほどほど悪くはない近似具合でした。

ピコカキコ - WAV9で音階付きメロディ楽器を鳴らす方法・ポイント・仕組み

このピコカキコを聴くと、ストリングスの音が綺麗になっています。音階が必要なサンプル波形がどうしてこのように鳴らせるのでしょうか?

FlMMLDPCMLFO をかけると、通常のメロディ波形のように音程が取れるようになります。波形のループも可能です。上記MMLではこの技を使っています。

DPCM 楽器の具体的な使い方

  1. DPCM Converter などで音色の定義を作成(FamiTracker など他のツールでも可、DPCMの制約については後述の「ループ波形を作成する際の注意」も参照)
  2. 音色指定時に下記のように @L を伴って指定する(音量を持続したければ @E1 も忘れずに)
/* 音色指定 */
@9-0 @L3882,0,2 @E1,0,0,350,0

/* 発音テスト */
o4a

/* 波形定義:波形再生ループ時は3番目のパラメータを1にする */
#WAV9 0,64,0,...

/* おまけ:ちなみに、普通に LFO が使いたい場合、音程補正は @D でも NS でも代用可 */
@9-0 @L50,48,0,24,2 @E1,0,0,350,0 @D3882

@L の記述はディチューンと同等です(ただし負数は指定不可)。最初のパラメータは cent 単位の音程指定なので、これを±100して音階を調節します。では下二桁は00で良いのかというとそうではなく、この値は入力サンプルレートに依存します(ただし、後述のとおり自由なサンプルレートが使えるので、下記の表は気にしなくて良いです)。

番号 サンプルレート @L指定値(下二桁) @L指定値(o4a/220Hz基準)
0 4181.71 Hz 02 or 98 298
1 4709.93 Hz 04 or 96 504
2 5264.04 Hz 03 or 97 697
3 5593.04 Hz 02 or 98 802
4 6257.95 Hz 04 or 96 996
5 7046.35 Hz 02 or 98 1202
6 7919.35 Hz 04 or 96 1404
7 8363.42 Hz 02 or 98 1498
8 9419.86 Hz 04 or 96 1704
9 11186.08 Hz 02 or 98 2002
10 12604.03 Hz 08 or 92 2208
11 13982.60 Hz 12 or 88 2388
12 16884.65 Hz 15 or 85 2714
13 21306.82 Hz 17 or 83 3117
14 24857.95 Hz 16 or 84 3384
15 33143.94 Hz 18 or 82 3882

音程の上限としては @L2400,0,2 の場合で o8g+ まで出せるようです。@L3600,0,2 のように補正を1オクターブ上げると、指定可能なノートの上限は o7g+ に下がります。サンプルレートが上がるほど、高い音は出なくなります。

波形あたりのサンプル数は、最大 32647 サンプルです。

高品質な音色を使うポイント「サンプルレート」

上記の情報を元に 33.14 kHz の DPCM を作成しても、波形のノイズ等に満足できないこともあると思います。これを解決するにはさらに大きくサンプルレートを上げてしまいましょう。

解決法はさまざまですが、手軽な方法を一例として紹介します。聞々ハヤえもんで波形を読み込んで、「再生周波数 25.0%」にしたファイルを保存・DPCM変換しましょう。きっと感じがだいぶ変わると思います。(なお、ごちゃ版の DPCMConverter.swf であれば、高サンプルレートのWAVEファイルをリサンプリングなしで変換することも可能です)

サンプルレートを上げることで、品質向上が狙える可能性があります。その理由は2つ。

  • 高域が失われないため、こもった感じがなくなる(一般的なハイサンプリングの利点)
  • ファミコンDPCMは差分を1bit(±1)で記録するため、サンプル同士の変化が激しいと誤差が大きくなり劣化を生む。同じ鳴り方をする波形でも高サンプルレートにすることでサンプル間の差が小さくなれば、誤差が軽減されて音質が改善される可能性がある。
    • DPCMは「変動なし」という情報を記録できないため、サンプル間の差が小さすぎる場合にも振動による誤差が生じる。差が大きすぎる場合に比べて弊害は少ないが、サンプル数を上げれば必ず音質が改善されるとは言えないことに注意。

一方で、下記のようなデメリットがあります。

  • 波形の長さ(秒)はサンプルレートに反比例して短くなります。
  • サンプルレートを上げると音程補正の度合いも大きくなる分、鳴らせる音階の幅はせまくなります。

このあたりの調整は、ひとつの頑張りどころと言えそうです。

任意のサンプルレートを持つ波形が o4a で鳴るようにするための cent 補正値が知りたい場合、下記の式を Google に計算してもらえばOKです。

(log(任意のサンプルレート / 220 / 16) * 1200) / log(2)

DPCM を作成する際の注意(特にループ波形)

下記の点に留意する必要があります。

  • 常に波形の先頭からループする(中間にループポイントは設定不可)
  • 波形サイズは 1+16n バイトでなくてはなりません(ループに関わらず NES DMC の仕様)
    • ピコカキコのプレイヤーはこの制約をちゃんと守っており、余剰バイトは内部で捨てられます。
    • 波形サイズは 1+16n バイトというのは、レート変換後の波形サンプル数が 8+256n でなければならないことを意味します。厄介ですが仕様なので仕方ありません。
  • DPCM がループする際に音量値は初期値に戻らない。つまり1周目と2周目で波形が異なってしまう可能性がある。
  • 最大サイズは $FF1 バイト、サンプル数にして 32648 サンプル。

使用するコンバータはお好みで良いのですが、私見を以下に記します。

  • arche さん作の DPCMConverter.swf には致命的なバグ*1があるので使わないほうが良いです(2013年5月2日時点)。また、DPCMのサイズ境界(1+16n)も考慮されていないため、末尾のサンプルが消失してしまう問題も抱えています。本記事でリンクしているバージョンはいずれも修正済であるため問題ありません。
  • 細かい違いはありますが、FamiTracker (PCMImport.cpp) と DMCconv の変換処理はほぼ同等です。FamiTracker の方が高品質なリサンプリングを行う。どちらも入力サンプル数が8で割り切れない場合は余った末尾サンプルを捨てます。どちらも最初の出力サンプルは初期値になりません(初期値同士を比較して符号化しているため)。

ポイントをまとめると以下のようになります。

  • バグがある DPCMConverter.swf は使わない。あとは各自の好みでOK。
  • ループ波形は長さを 8+256n サンプルにしなければならない。
  • ループ波形は1周目と2周目で異なる波形を出力しうる。波形の終端で音量が大きく元に戻らない場合、波形終端をフェードアウトしたり、サンプルレートを上げたりすることで収束しやすくなるかもしれない。
  • 変換前にLPFをかけておくと、波形が滑らかになってDPCM変換向きになる可能性がある(眉唾)

おまけ:LFO で音階利用の仕組み周り

本来、WAV9 の DPCM は指定するノートによって16段階の周波数で発音できます(周波数は表にもとづいていて、音程差は均等ではない)。LFO コマンドを使うとリアルタイムで再計算が必要になる都合により、周波数の計算が他の波形と同じような形になります。具体的にソースコードで言うと、普通は MOscFcDpcm.as の setNoteNo() で周波数が決まるところを、LFO が setFrequency() によって周波数を書き換えてしまいます。一般的な波形では o5a を 440 Hz で再生しますが、setFrequency() では値を16倍しているため、WAV9 では o5a が 7040 Hz で鳴ります。

LFO の内容は長さ0の三角波です。長さ0なので振動しません。仕様的に三角波の変位は0から開始ではないため、指定した音程の変化が即時に行われるわけです。

これがわかれば、あとはひたすらどのように高品質な波形を用意するかが問題だと思います。上記の通り気をつけなければいけないことが多々ありますが、がんばるしかないです。

何か素敵な技がありましたら、ぜひぜひおしえてください。 (ノヮ=*)

おまけ:DPCMのサンプル長

Bytes Samples
$001 8
$011 136
$021 264
$031 392
$041 520
$051 648
$061 776
$071 904
$081 1032
$091 1160
$0A1 1288
$0B1 1416
$0C1 1544
$0D1 1672
$0E1 1800
$0F1 1928
$101 2056
$111 2184
$121 2312
$131 2440
$141 2568
$151 2696
$161 2824
$171 2952
$181 3080
$191 3208
$1A1 3336
$1B1 3464
$1C1 3592
$1D1 3720
$1E1 3848
$1F1 3976
$201 4104
$211 4232
$221 4360
$231 4488
$241 4616
$251 4744
$261 4872
$271 5000
$281 5128
$291 5256
$2A1 5384
$2B1 5512
$2C1 5640
$2D1 5768
$2E1 5896
$2F1 6024
$301 6152
$311 6280
$321 6408
$331 6536
$341 6664
$351 6792
$361 6920
$371 7048
$381 7176
$391 7304
$3A1 7432
$3B1 7560
$3C1 7688
$3D1 7816
$3E1 7944
$3F1 8072
$401 8200
$411 8328
$421 8456
$431 8584
$441 8712
$451 8840
$461 8968
$471 9096
$481 9224
$491 9352
$4A1 9480
$4B1 9608
$4C1 9736
$4D1 9864
$4E1 9992
$4F1 10120
$501 10248
$511 10376
$521 10504
$531 10632
$541 10760
$551 10888
$561 11016
$571 11144
$581 11272
$591 11400
$5A1 11528
$5B1 11656
$5C1 11784
$5D1 11912
$5E1 12040
$5F1 12168
$601 12296
$611 12424
$621 12552
$631 12680
$641 12808
$651 12936
$661 13064
$671 13192
$681 13320
$691 13448
$6A1 13576
$6B1 13704
$6C1 13832
$6D1 13960
$6E1 14088
$6F1 14216
$701 14344
$711 14472
$721 14600
$731 14728
$741 14856
$751 14984
$761 15112
$771 15240
$781 15368
$791 15496
$7A1 15624
$7B1 15752
$7C1 15880
$7D1 16008
$7E1 16136
$7F1 16264
$801 16392
$811 16520
$821 16648
$831 16776
$841 16904
$851 17032
$861 17160
$871 17288
$881 17416
$891 17544
$8A1 17672
$8B1 17800
$8C1 17928
$8D1 18056
$8E1 18184
$8F1 18312
$901 18440
$911 18568
$921 18696
$931 18824
$941 18952
$951 19080
$961 19208
$971 19336
$981 19464
$991 19592
$9A1 19720
$9B1 19848
$9C1 19976
$9D1 20104
$9E1 20232
$9F1 20360
$A01 20488
$A11 20616
$A21 20744
$A31 20872
$A41 21000
$A51 21128
$A61 21256
$A71 21384
$A81 21512
$A91 21640
$AA1 21768
$AB1 21896
$AC1 22024
$AD1 22152
$AE1 22280
$AF1 22408
$B01 22536
$B11 22664
$B21 22792
$B31 22920
$B41 23048
$B51 23176
$B61 23304
$B71 23432
$B81 23560
$B91 23688
$BA1 23816
$BB1 23944
$BC1 24072
$BD1 24200
$BE1 24328
$BF1 24456
$C01 24584
$C11 24712
$C21 24840
$C31 24968
$C41 25096
$C51 25224
$C61 25352
$C71 25480
$C81 25608
$C91 25736
$CA1 25864
$CB1 25992
$CC1 26120
$CD1 26248
$CE1 26376
$CF1 26504
$D01 26632
$D11 26760
$D21 26888
$D31 27016
$D41 27144
$D51 27272
$D61 27400
$D71 27528
$D81 27656
$D91 27784
$DA1 27912
$DB1 28040
$DC1 28168
$DD1 28296
$DE1 28424
$DF1 28552
$E01 28680
$E11 28808
$E21 28936
$E31 29064
$E41 29192
$E51 29320
$E61 29448
$E71 29576
$E81 29704
$E91 29832
$EA1 29960
$EB1 30088
$EC1 30216
$ED1 30344
$EE1 30472
$EF1 30600
$F01 30728
$F11 30856
$F21 30984
$F31 31112
$F41 31240
$F51 31368
$F61 31496
$F71 31624
$F81 31752
$F91 31880
$FA1 32008
$FB1 32136
$FC1 32264
$FD1 32392
$FE1 32520
$FF1 32648

*1:DACは7bitだが、波形の変動は下位1bitを無視した6bitの範囲で行うのが正しい。しかし、arche さんのコンバータは単なる7bitで扱っているため、内部で想定する波形がまったく異なってしまう。

ピコカキコ - WAV13によるPCM再生の考え方

WAV13によるPCM再生とは何か、例えばこのピコカキコを聴いてみてください。

両サイドで鳴っているパッドの音、それにドラムの音、とても綺麗です。どちらもトリッキーな鳴らし方をしているようですが、この記事ではドラムで使われている技法を主に追求します。

ピコカキコ WAV13 PCM利用補助ツールを作成しました。後述の面倒な計算が一瞬で行えます。実用的な面だけ知りたい人は、まずツールのページを読んで、それから情報を本記事で補うと良いかもです。

美しいPCMの鳴らし方 ~ロジックとトリック

ドラムの音など、サンプリングした音色を鳴らしたい場合、普通はファミコンDPCMに相当するWAV9を使用します。しかし、WAV9の性能はファミコン由来なので音質は低く、音程も15段階でしか鳴らせません。普通はパーカッションはDPCMで妥協し、音色は各種波形でなんとか近いものを作成することになります。

知る限り、これを克服して品質の高いサンプルを鳴らすトリッキーな方法が3つあります。(熟練の人はもっと知っているかも)

  • ピコカキコ、楽しいよ! のPCMテストで知られる手法(PCM変換スクリプトと説明)。テンポを超高速にする必要があるため、単発で鳴らさなければならない。
  • WAV9 の DPCM に @L コマンドで LFO をかけると音程が取れるバグを利用した高サンプルレート再生*1(上記ピコカキコの両側のパッド音で使用)
  • WAV13 の波形メモリを超低音で鳴らして任意の8bit波形を鳴らす。後ほど考察するが、音程や発音の長さを巧みに調整する必要がある*2(上記ピコカキコの両側のドラム音で使用)

本記事では、一番最後の手法を自身の楽曲に組み入れたい場合の考え方を考察します。わかりやすく書きたいところですが、話の性質上MMLや波形に関する一定の知識が必要です。

WAV13 の特徴

WAV13の説明は FlMML - Documentation にありますが、可変長(長さ上限あり)の符号なし8bit波形メモリ音源です。

本来は周期的に繰り返される短い波形を扱うものです。1周期分の波形を16進数で書いて波形定義します。なので、例えば o5a (440 Hz) のノートを発音すると、定義した波形が1秒間に440回繰り返される周波数で再生されます。

例えば、1024サンプルの波形を定義したとして、440 Hz で鳴らすと再生周波数は 440×1,024=450,560 Hz というとんでもない高さになります。代わりに、例えば音階をぐっと下げて 7.8125 Hz で鳴らすと 7.8125×1,024=8000 Hz という現実的な WAVE ファイルらしい再生周波数になります。超低音ノートで鳴らすことで WAV13 は 8bit PCM として活用できるのです。

WAV13 を利用する問題点

WAV13をワンショットPCMとして使用するには下記の問題点があります。

  • 【問題】周期波形であるため、発音が波形終端に達した後で先頭に戻ってしまう。
    • 【対策】発音する長さをtick単位で制御してループ直前で止める。休符を併用してリズムを取る。(実質マクロ利用が必須)
  • 【問題】1024サンプルは短く、な波形を一つ鳴らすにも不自由することが少なくない。
    • 【対策】複数のWAV13定義波形をシームレスに鳴らして1024サンプルの壁を超える。
      • 【問題】テンポが変動すると波形がうまくつながらなくなる。特効薬はない。

なんだかむつかしくないですか!? わたしはこれで半日も悩みました!

実用へのアプローチ(理論的な話)

上述の通り、音程や音長をシビアに調整しなければなりません。最も解像度が低く調整困難なのはノートの長さです。よって、1つの波形が何tickに相当するのか決定するところから、各種の調整を行います。

  1. バンクあたりの再生時間(tick単位)を決定する
  2. バンクの再生時間とサンプル数からサンプリングレートを求める(リサンプリングに使用)
  3. バンクの再生時間から再生周波数を計算し、音階とディチューンの値を求める
  4. 求めた音階とディチューンと再生時間で入力波形を鳴らす

このような順序になります。音色定義時のサンプル数ですが、サンプル数が変動すると想定サンプリングレートが変化してしまうため、定義は一律最大サンプル数になるように最終サンプルを複製します。

細かく計算式を書くと面倒なので、知りたい方は補助ツールのソースコードから読み解いてください。

計算に必要な定数

簡単に WAV13 PCM サンプルを使う方法!

上記の説明は概念的過ぎますし、詳細が理解できる人でもいちいち計算するのは面倒です。なので、ピコカキコ WAV13 PCM利用補助ツールを作成しました。面倒で謎に包まれた計算が一瞬で行えます。

  1. 上記ツールにテンポや再生時間の情報を適当に入力する
  2. 波形ファイルを「PCM サンプリングレート」に極力近い値にリサンプリングして保存 (Microsoft WAVE, 8 bit モノラル PCM, サンプリングレートの変換には SSRC などのツールを利用)
  3. Stirling などのバイナリエディタで波形部分 (data セクション部) を抜き出し、WAV13 波形として定義する。
    1. 長さが「最大サンプル数」を超える場合は複数の波形定義に分割する。
    2. 長さが「最大サンプル数」より短い定義は、最終バイトを複製して「最大サンプル数」まで増やす。(ただし、仕組みを理解していれば短いまま使うこともできる。「最大サンプル数」を変更して計算を行い、得られたノート再生コマンドを使えばよいだけの話)
  4. 「ノート再生コマンド」で波形が鳴ることを確認する。(波形を分割している場合、@13-? o0b%12 ~ の部分を異なる波形で連続して行えば良い)
  5. 音声再生マクロを作成すると楽曲での使い勝手が格段に向上する(WAV13利用例 参照)

以上です! ぜひ素晴らしいピコカキコを作ってください!

*1:バグのようなので挙動が保証されないのが問題ですが、音程を変更しやすいのでメロディ音色に使えます。これはWAV13では乏しい利点です。

*2:2つ以上のサンプルをつなげて使うことが多いですが、そうすると特にテンポ変更や音程変更ができないので、音程の固定されたパーカッションなどに使うことになります。

MIDIトラック分割ツール「MidiSplit」を作りました

MidiSplit project page (Download) - Divide MIDI track by channel number & program number

MIDIファイルを読み込んで、楽器ごとにトラックを分割するツールを作りました。ツールの概要ですが、下記の画像をご覧ください。

MidiSplit は主にチャンネル数を制限したシーケンスの再配置に有効です。チャンネル数を節約しているシーケンスでは曲中でプログラムチェンジを送ることで、1チャンネル(1トラック)で複数のパートを演奏していることがあります。そのようなシーケンスをアレンジしようとする場合、パート(楽器)ごとに閲覧や調整ができたほうが便利ですが、プログラムチェンジが頻繁に配置されている場合は分割するのに手間がかかります。

MidiSplit はトラックに混在する各パートの演奏を手早く楽器ごとに分割します。

仕様および注意点

  • MidiSplit は各トラックを先頭から走査し、チャンネル番号とプログラム番号とバンク番号を基準に演奏内容を分割します。
  • トラック分割は、プログラムチェンジを基準に行われます。このとき、プログラムチェンジの前に出現したチャンネルメッセージ(ノート類を除く)も同時に新規トラックへと移されます。
  • SysExなどチャンネルメッセージ以外のメッセージは、先頭の分割後トラックに格納されます。
  • ドラムチャンネルの区別はしておらず、チャンネル10でも他のチャンネルでも同様に動作します。
    • v1.2 で -sp というオプションを追加、ノート番号別に特定のチャンネルや楽器を分割できるようになりました。
  • バンク変更はプログラムチェンジ受信時に反映されるものとして扱います。
  • 16チャンネル以上のMIDIデータには正式対応はしていませんが、メタイベント0x21は分割後トラックすべてに出力するようにしてあるため、同イベントを用いたフォーマット1のMIDIデータであれば、おそらく問題なく処理することが可能です。
    • 不具合がありましたが、v1.2 で修正しました。

DeSmuMEに3D表示抑制機能を足してみた

Download Windows binary & source code (desmume 0.9.8-gfx3dHack)

ゲーム中のキャラクタの画像だけとか、地形の画像だけキャプチャしたい時ってありますよね。他のエミュレータと同じくDeSmuMEにもレイヤー表示・非表示を切り替える機能はあるのですが、ゲームによっては前景から背景に至るまで同一レイヤーで3D描画していることがあり、この場合には前述の機能は役に立ちません。

じゃあ何かしら役に立つ機能をつけてみようというのが本日の趣旨です。

改造の概要

「3D描画を部分的に抑制したい」というのが今回求める機能です。

内部的には3Dデータは多数のポリゴンで構成されています。これを位置情報に基づいて、3Dエンジンが背面〜前面にかけて描画することで処理されます。表示を抑制したければ、一部のポリゴンの描画処理をあえて行わなければ良いのです。簡単ですね。

レイヤーと同様にポリゴンの表示・非表示機能をつければ良さそうですが、困ったことにポリゴンの数は軽く100を超えるくらいには多いです。個別切り替えでは使い勝手もよくなさそうなので、「前の方のポリゴンを非表示」「後ろの方のポリゴンを非表示」という形で対応することにしました。「前の方」「後ろの方」の度合いを切り替えできるようにすることで、抑制度合に柔軟性をもたせます。

コードと使い方

0.9.8 のソースコードとの差分を作成して、改造版を作成しました。ダウンロードリンクは記事の冒頭にあります。

新しくダイアログを設けるのは実装の手間がかかるので、表示の抑制はLuaスクリプトを通じて行う形にしました。

-- desmume 0.9.8-gfx3dHack sample script

local gfx3dMin, gfx3dMax = 0.000000, 1.000000
function modifyVisibility()
	gui.text(0, 0, string.format("%f %f", gfx3dMin, gfx3dMax))
	gui.setgfx3dvisibility(gfx3dMin, gfx3dMax)
end

local keys = { {}, {} }
gui.register(function()
	keys[1] = input.get()

	if keys[1]["6"] then
		gfx3dMin = math.min(math.max(gfx3dMin - 0.001, 0.0), 1.0)
	end
	if keys[1]["7"] then
		gfx3dMin = math.min(math.max(gfx3dMin + 0.001, 0.0), 1.0)
	end
	if keys[1]["8"] then
		gfx3dMax = math.min(math.max(gfx3dMax - 0.001, 0.0), 1.0)
	end
	if keys[1]["9"] then
		gfx3dMax = math.min(math.max(gfx3dMax + 0.001, 0.0), 1.0)
	end

	modifyVisibility()

	keys[2] = keys[1]
end)

emu.registerexit(function()
	gui.setgfx3dvisibility(0.0, 1.0)
end)

追加した関数は1つだけです。

function gui.setgfx3dvisibility(gfx3dStart, gfx3dEnd)
-- gfx3dStart: 0.0 = no effect, 1.0 = hide all polygons
-- gfx3dEnd: 0.0 = hide all polygons, 1.0 = no effect

3Dポリゴンの表示割合を設定します。最前面・最背面にあるポリゴンの一部を隠すことができるようになっていて、例えば 0.2, 0.9 と指定すると、全ポリゴンのうち、最背面にある 20% のポリゴンと、最前面にある 10% (100%-90%) のポリゴンは表示されなくなります。サンプルスクリプトでは、数字の 6, 7, 8, 9 キーを使ってこの表示割合を変更できるようにしてあります(スクリプト自体の利便性は高くないので、目的に応じて改良することができると思います)。

感想

万能ではないですが、良い感じに欲しい画像が得られるようになりました。

総ポリゴン数は細かい状況の違いによって簡単に変動するので、少しゲームを操作すると表示がちらついて、思うような表示結果が継続されないという利便性の問題があります。不便さはありますが、数値を細かく調整する、欲しいフレームを狙ってキャプチャするなど、工夫次第で目的は果たせるかなあと思います。

あとは背景色が自前で指定できるとか、描画のない部分はアルファチャンネル付きの透明ピクセルとして保存できるようにするとか、そんな対応があると背景の除去や合成がぐっとしやすくなると思うのですが、今日のところは放っておくことにします。誰か興味があれば改良してくれると喜びます。

それでは、良いリッピングライフをお楽しみください☆

追伸

背景色の操作ですが、Memory Viewer で Palette 0 を操作したら変更できました。15 bit color かな?

  5000000h Engine A Standard BG Palette (512 bytes)
  5000200h Engine A Standard OBJ Palette (512 bytes)
  5000400h Engine B Standard BG Palette (512 bytes)
  5000600h Engine B Standard OBJ Palette (512 bytes)
  7000000h Engine A OAM (1024 bytes)
  7000400h Engine B OAM (1024 bytes)
  http://nocash.emubase.de/gbatek.htm#dsmemorycontrolvram

Soft Rasterizer はピクセルずれを起こすのでオススメ出来ません。OpenGLレンダリングしましょう。条件によっては選びたいエンジンが変わるかもしれませんが。

Lua-GD でのアルファチャンネルの扱い

Lua-GD のアルファチャンネルの扱いでよく失敗するのでまとめました。勘違いしていると、じつは画像が透明だったり、不透明だったり、半透明があるのに思い通りにブレンドされなかったりします。

内容は Lua-GD 2.0.33r2 に準拠しています。他の環境下での挙動は保証しません。

画像の新規作成 (フルカラー)

  • フルカラー画像の作成には、gd.createTrueColor(x, y) を利用。
  • 画像作成後は、全面不透明黒で塗られた状態(透明ではない)
  • 画像作成後は、gdImage:alphaBlending(true) の状態。
  • 画像作成後は、gdImage:saveAlpha(false) の状態。
  • アルファチャンネルの範囲は 0〜127 であり、0〜255 ではないので注意。
  • 透明なキャンバスを作成するには透明ピクセルで塗りつぶす必要がある。下記にフルカラー透過画像を作成するコードを示す。
-- create a blank truecolor image
gd.createTrueColorBlank = function(x, y)
	local gdImage = gd.createTrueColor(x, y)
	if gdImage == nil then return nil end

	local colorTrans = gdImage:colorAllocateAlpha(255, 255, 255, 127)
	gdImage:alphaBlending(false)
	gdImage:filledRectangle(0, 0, gdImage:sizeX() - 1, gdImage:sizeY() - 1, colorTrans)
	gdImage:alphaBlending(true)
	gdImage:colorDeallocate(colorTrans)
	return gdImage
end

余談ですが、gd.create(gdImage) 的な関数はありそうでないです。

画像の読込 (PNG)

  • PNG画像の読込には、gd.createFromPng(filename) を利用。
    • ファイル内容を文字列をとして読み込んである場合は gd.createFromPngStr(string) を利用。
  • パレットかフルカラーかは入力ファイルに基づく(要注意!

画像の保存 (PNG)

  • PNG画像の保存には、gdImage:png(filename), gdImage:pngEx(filename, compression_level) を利用。
  • 出力ファイルがパレットかフルカラーかは入力画像に基づく。
  • アルファチャンネルの保存は gdImage:saveAlpha(true) を実行していないと行われない。

画像のコピー・貼り付け

  • 画像の一部を転写するには gd.copy(dstImage, srcImage, dstX, dstY, srcX, srcY, w, h) を利用。
  • フルカラー画像は dstImage:alphaBlending(boolean) の設定により挙動が異なる。true であればブレンディングを行うが、false であればアルファ値を含めてピクセルをそのままコピーする*1
  • フルカラーとパレットの混在時には注意を要する(後述)

パレットとフルカラー

フルカラーとパレットの混在時には注意を要する場合がある。

  • 画像がフルカラーかどうか調べるには、下記のようなメソッドを要する。
gd.isTrueColor = function(gdImage)
	if gdImage == nil then return nil end
	local gdStr = gdImage:gdStr()
	if gdStr == nil then return nil end
	return (gdStr:byte(2) == 254)
end
  • パレットの「透過色」とフルカラーの「アルファ」は異なる概念である。それ故か下記の問題がある。
    • gd.copy() で パレット→フルカラー のコピーを行うとき、gdImage:alphaBlending(boolean) に関わらず透過ピクセルが透過コピーされてしまう。ただし、gd.copyResampled() であれば gdImage:alphaBlending(boolean) を尊重したコピーが行われる。
  • 上記を考慮すると、画像をフルカラーに変換する下記のコードが書ける。
-- return a converted image
gd.convertToTrueColor = function(srcImage)
	if srcImage == nil then return nil end
	if gd.isTrueColor(srcImage) then return srcImage end

	local gdImage = gd.createTrueColor(srcImage:sizeX(), srcImage:sizeY())
	if gdImage == nil then return nil end

	gdImage:alphaBlending(false)
	local colorTrans = gdImage:colorAllocateAlpha(255, 255, 255, 127)
	gdImage:filledRectangle(0, 0, gdImage:sizeX() - 1, gdImage:sizeY() - 1, colorTrans)
	gdImage:copyResampled(srcImage, 0, 0, 0, 0, gdImage:sizeX(), gdImage:sizeY(), gdImage:sizeX(), gdImage:sizeY())
	gdImage:alphaBlending(true)

	return gdImage
end

おまけ:画像の左右反転

  • 拡大コピーで負数を指定
  • 反転画像の作成は、新しいGDライブラリにはあるが、上記 Lua-GD にはない。ただし下記で代用できる(巨大画像では低速)
-- flip an image about the vertical axis
gd.flipVertical = function(gdImage)
	if gdImage == nil then return nil end
	gdImage:alphaBlending(false)
	for x = 0, gdImage:sizeX() do
		for y = 0, math.floor(gdImage:sizeY()/2) - 1 do
			local c1, c2 = gdImage:getPixel(x, y), gdImage:getPixel(x, gdImage:sizeY()-1-y)
			gdImage:setPixel(x, y, c2)
			gdImage:setPixel(gdImage:sizeX()-1-x, y, c1)
		end
	end
	gdImage:alphaBlending(true) -- TODO: restore the previous value
	return gdImage
end
-- flip an image about the horizontal axis
gd.flipHorizontal = function(gdImage)
	if gdImage == nil then return nil end
	gdImage:alphaBlending(false)
	for y = 0, gdImage:sizeY() do
		for x = 0, math.floor(gdImage:sizeX()/2) - 1 do
			local c1, c2 = gdImage:getPixel(x, y), gdImage:getPixel(gdImage:sizeX()-1-x, y)
			gdImage:setPixel(x, y, c2)
			gdImage:setPixel(gdImage:sizeX()-1-x, y, c1)
		end
	end
	gdImage:alphaBlending(true) -- TODO: restore the previous value
	return gdImage
end
-- applies vertical and horizontal flip
gd.flipBoth = function(gdImage)
	-- use of gd.copyRotated() can provide the same result?
	gd.flipVertical(gdImage)
	gd.flipHorizontal(gdImage)
	return gdImage
end

おまけ:EmuLua

  • 今日以前の desmume は gui.gdscreenshot() で透明な画像を返す
    • gd.createFromPngStr(gd.createFromGdStr(gui.gdscreenshot()):pngStr()) と書けば1行でアルファを切り捨て可能

*1:srcImage が完全に透明な画像を考えるとわかりやすい。true の際は dstImage に変化はないが、false の際は dstImage のコピー部分が透明ピクセルに変化する。