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

高見知英のかいはつにっし(β)

高見知英のアプリケーション開発日誌 のほか、地域活動などの活動報告ブログ。

BGMのループ再生

たまには開発日誌らしいことを。

自分は自宅で作業をするとき、よくゲームの戦闘曲を聞いています。元々戦闘シーンのBGMとして作られている曲なだけあって、これらの曲を流していると作業に集中できます。ゲームのサウンドトラックCDを購入するなどして集めた曲を一曲リピートでずっと流しながらの作業が、最近続いています。


しかし、それでも集中を阻むもの。それは「普通のサウンドトラックの曲データは、ゲーム内BGMのように、ループ再生ができない」ということ。いくら戦闘曲とはいえ1ループはそれほど長くなく、だいたい1分ちょっとで終わってしまいます。サウンドトラックのデータは、大体2~3ループぶん収録されてますが、それでも2,3分で終わり。この曲の切れ目の無音期間と、再度イントロから始まってしまう感覚が、どうしても集中力を削いでしまいます。
いちおうMP3の波形をいじって同じ曲が延々とループする曲を作ることは可能ではありますが、データ容量を食うし、なにより手間がかかるのであんまり沢山作るわけにはいかない*1。ゲーム中と同様、BGMを無限ループさせるような音楽プレイヤーはないものか…。

まずは、探す。

とりあえず、Windows用とAndroid用を中心に、いくつか探してみます。RPGツクールなどは楽譜データであるMIDIファイルには拡張コマンドを、オーディオデータであるOGGファイルに拡張ヘッダを用意することで、正確なBGMループ機構を用意しています。しかしどちらも当然正式仕様ではないので、一般的に使える音楽プレイヤーはそれをサポートしていない。

一応、OGGファイルの拡張ヘッダを読み込むプレイヤーはありましたが…。

ただ、RPGツクールの拡張ヘッダはサンプル単位という非常に高精度なループカウンタを指定しているのに対し、こちらはそれをミリ秒単位で判別してしまうためか、ループ精度がイマイチよくなく、曲によってはループ端が非常に目立ってしまいます。

他にもそれらしい方法を探してはみましたが、プログラム上からOGGファイルのループを実現する方法(XNAゲームスタジオあたりには、そういう機能があるらしい?)はあるものの、実際の音楽プレイヤーについてはなかった模様。

・・・ということで、作成してみた

さらに探してみると、HSPのhmm.dllという拡張プラグインは、ミリ秒単位でのOGGファイル無限ループをサポートしているらしい。ミリ秒単位なのでずれが怖かったですが、他にはないのでひとまずこれでやってみる。
f:id:TakamiChie:20141108232126j:plain
HSPはあまり分からないので、非効率な部分はあるかも。

#include "hmm.as"
#include "llmod3/llmod3.hsp" 
#include "obj.as" 
#include "hspdef.as"
#packopt name "bgmplayer"


*init
	; 初期化処理
	scrx = 208: scry = 320
	screen 0, scrx, scry
	pos 4 , 4
	mes "BGM Player"
	pos 4, 30
	button "再生" , *playfile
	pos 80, 30
	button "ポーズ" , *pausefile
	dirlist dir, "*.ogg", 0
	listbox list , 300 , dir
	s = 200,120,4,60
	resizeobj 2, s
	pos 4, 300
	chkbox "継ぎ目チェックモード", checkmode
	s = 120,20
	resizeobj 3, s, 1
	pauseflag = 0
	title ""
	onexit *exit

dshinit
if stat == 0: end
stop

*playfile
	; 再生ボタン押下
	if( pauseflag == 1){
		; ポーズ→再開
		dshplay 0, 1, -1, playstart, playstart + playlen
	}else{
		; 再生開始
		pauseflag = 1
		notesel dir
		noteget fn, list
		conffn = dir_cur + "\\" + strmid(fn, 0, strlen(fn) - 3) + "txt"
		file = dir_cur + "\\" + fn
		playstart = 0
		playlen = 0
		exist conffn
		if( strsize != -1 ){
			; confファイルを読み込む 一行目 ループの開始位置、二行目、ループの長さ(ともにミリ秒)
			notesel conf
			noteload conffn, -1
			noteget ps, 0
			noteget pl, 1
			playstart = int(ps)
			playlen = int(pl)
		}
		; 再生失敗エラーの発生頻度抑制
		dshend
		await 1000
		dshinit
		dshloadfname file, 0
		if(checkmode == 1){
			; 継ぎ目チェックモード 継ぎ目のちょっと前から再生してみる
			dshplay 0, 1, -1, playstart, playstart + playlen
			dshsetseek 0, playstart + playlen - 2000
		}else{
			; ふつうに再生
			dshplay 0, 1, 0, playstart, playstart + playlen
		}
	}
	title "再生中"
	pauseflag = 0
	goto *playloop
	stop

*pausefile
	; ポーズボタン押下
	pauseflag = 1
    dshpause 0
	title "ポーズ"
    stop

*playloop
	await 10
	; プレイループ
	if(pauseflag != 1){
		; ポーズ中でなければ、再生位置情報の取得・表示
		dshgetplayposition 0, playposition, stopposition
		gosub *__redraw
		pos 4 , 180
		mes "再生中\n@" + strf("%06d", playposition) + "ms"
		if(playstart != 0){
			mes "ループ開始位置:" + playstart + "ms"
			mes "再生範囲:" + playlen + "ms"
		}else{
			mes "ループ位置未指定"
		}
		redraw 1
		goto *playloop
	}else{
		; 再生位置情報の削除
		gosub *__redraw
		redraw 1
	}
	stop

*__redraw
	; 再描画処理
	redraw 0, 4, 180, 329, 329
	color 255, 255, 255
	boxf 4, 180, 320, 320
	color 0, 0, 0
	return
	
*exit
	dshend
	end

ソースを見て察しは付くかもしれませんが、このアプリ(ソースコード)があるのと同じフォルダのOGGファイルをリストアップし、指定したファイルを再生するだけです。ただし、OGGファイル再生時に、同じファイル名のテキストファイルが同じフォルダにあれば、その中身を読み込み、以下のルールでループ位置を設定しています。

  • 一行目に書かれている数字を、ループ開始のミリ秒とみなす
  • 二行目に書かれている数字を、ループ期間のミリ秒とみなす

自分専用なのでエラーチェックは一切していません。たぶんおかしなデータを置いていたりしたら処理が止まります。

実際これで手持ちのファイルを再生してみたところ、多少気になるところもなくはないものの、実用は何とか可能。OGGファイルはWindows標準では再生できないので、Audacityなどでファイルを変換した上、Windows Media PlayerでOggファイルを再生する | 豆知識を参考にコーデックを入れないと再生できないなどと、面倒ではありますが、とりあえず集中は維持できる程度のレベルにおさまっているので、とりあえずこれで使ってみています。

なお、今のところ確認できてる問題点として、特に継ぎ目チェックモード(ループポイントの二秒前から再生し、ループ端の接続状態を確認するモード)の動作が怪しく、ちょくちょく強制終了します(調子が悪いと、それ以外の場所でも、ちょくちょく落ちます。ひょっとしたらどこかでメモリリークでもしてるのかもしれない)。が、とりあえずBGMプレイヤーとしての役目は果たしているので、これでよし。

そのほか

参考までに、ひとまず自分の手持ちデータのループ情報をメモ。なお、今回ソースは全て公式サウンドトラックCDです。どなたかが耳コピして作成したMIDIファイルが元だったり、iTunes Musicで購入したデータが元だったりすると、若干違う可能性はあります。

曲名 ゲームタイトル ループ開始ミリ秒 再生範囲
Time To Make History Persona 4 Golden 6448ms 70713ms
Mass Desrruction - P3fes version Persona 3 Fes 8649ms 84713ms
I'll Face Myself - Battle - Persona 4 23894ms 79588ms
Period Persona 4 485ms 46500ms*2
Wiping All Out Persona 3 Portable 15884ms 78000ms
女神の騎士 FF13-2 8982ms 64886ms
名誉のファンファーレ FF13-2 10936ms 48042ms
祝福のファンファーレ FF13-2 18707ms 47991ms

理想としては、波形を見ればどこがループ端かはなんとなく分かると思うので、それで自動的にループするようなプログラムを作成することですが、自分はオーディオフォーマットには詳しくないし、そこまでの余力はなかったので、残念ながらそこまではできませんでした。

余力があれば、もうちょっと高精度なアプリを作れる環境で作り直したいですね。HSPも決して悪い環境ではないと思いますが、元々ゲーム用の言語なのでUI部品があまりにも貧弱ですし、動作が不安定なのも気になりますし。
調べてみたところ、C#用の音楽ライブラリにNAudioというものがあるそうなので、それを使えばもしかしたら実現可能かもしれないですね(ライブラリ自体は、Visual StudioのNuGetを使って導入可能)。


他によい方法、よいアプリなどあれば、教えていただけると助かります。

*1:それでも同じようなことを何度もやってきたので、すこしは手際よくループ部分を見つけることができるようになった

*2:サウンドトラックの状態ではゲーム通りのループが不可能なので、前奏以外すべてループにしています