HSP3でミュージックプレイヤーに歌詞表示機能を付ける(.lrcファイルに対応する)

ミュージックプレイヤーを作っていて、

"歌詞が表示できると良いな"

と思い軽く作ってみたら割と簡単に出来たのでメモ。

特にすごい技術使ったわけでも無いし特段ソースを非公開にする必要もないと思うし。

言語は「HSP3」で対応ファイル(形式)は「.lrc」

完成形はこれ。
1行目が再生中のタイム、2行目が次に表示する歌詞の行数と表示するタイム3行目が歌詞

 

0.はじめに

完成形(上)の画像を見ればわかるけど、UIは特段凝ったものじゃなく、

動作している事が分かるように適当な感じで表示しているだけ。

表示位置やデザインは各々で作って拘ってもらうとして、

ここでは特に扱わないものとする。

 

1.仕様を決める

対応する歌詞のファイル「lrc」には色々記載方法がある模様。

一応定義はあるっぽい?けど浸透しているのかは分からず、

ソフトによってまちまちな点もあるみたいなので、こちらで仕様を考える。

今回は下記の通り

・タイムタグは一行に一つまで、先頭にあるもののみ読み取る

・タイムタグは"[]"で囲む

・タイムタグは"mm:ss"の形式か"mm:ss:ff"の形式何方にも対応する

・タイムタグの記載は全て半角のみ、数字は2桁で記載する

・"mm"は00~99まで、"ss"は00~59、"ff"は00~99まで受け付ける

・"ss"と"ff"の間は":"と"."の二つに対応する

・行頭のタイムタグ1つ以降に記載された文字は全て歌詞として扱う

・以上のルール通りじゃない記載やタイムタグがない行、空欄の行は全てスルーする

上記の仕様に従って制作していく。予め歌詞ファイルを作っておこう。

 

2.歌詞ファイルからタイムタグと歌詞を成型、配列変数に入れていく

2-1.まずはlrcファイルを読み込む
	notesel tmp
	noteload "歌詞.txt"		;歌詞ファイル読み込み
	kasi_max = notemax		;行数を取得
	sdim kasi,,kasi_max+1,2		;歌詞の行数分配列変数用意

取り敢えずさっくり作ったのでtxtファイルのままなのですが、然程問題ありません。

それぞれの環境に合わせてファイル名やパスを変えてください。

実際には拡張子「.lrc」がメインになると思います。

読み込み後、幾つかの箇所で行数を使用したため、変数に行数を入れています。

そして、各行のタイムタグ部分と歌詞部分を取得して格納する為の配列変数を用意します。

行数分+1取得しているのは0番目を使用しない為。1行目を1番目として分かりやすくしたかった意図。

プログラマーだと0スタートになれてたりして逆に分かりにくいかもですが。

この2次元配列に歌詞を入れていきますが、n,0にタイム、n,1に歌詞を入れます。

2-2.一行ずつ取得し、歌詞部分を変数に入れる
	repeat kasi_max							;歌詞の行数分繰り返す
		getstr kasi(cnt+1,0),tmp,tmp_index			;一行ずつ取り出し
		tmp_index += strsize					;次の行の開始位置を計算
		tmp1 = instr(kasi(cnt+1,0),0,"]")			;タイムタグの最後の場所を入れる
		kasi(cnt+1,1) = strmid(kasi(cnt+1,0),tmp1+1,999)	;歌詞の内容のみを取得

※この時点ではrepeatがあるのにloopが無いのでエラー出ます。後の工程で追加します

repeat kasi_max

これから行う作業を全ての行で繰り返すためrepeatを使用し、先程取得した行数分ループします。

getstr kasi(cnt+1,0),tmp,tmp_index

tmp_index += strsize

getstr命令で1行分のみ取得し、strsizeで次の行を取得する開始位置を記録しています。

取得した内容を、作成した配列変数に入れてますが、特に意味はありません。

他の一時的に使用できる変数に入れたので問題ありません。

tmp1 = instr(kasi(cnt+1,0),0,"]")

タイムタグの記載で、最後の文字は"]"になるはずなので、その位置を取得します。

後程何度か使うことになる為、変数に記録をしています。

また、ミリ秒を記載するタイプとしないタイプのどちらにも対応するため位置を取得しています。

kasi(cnt+1,1) = strmid(kasi(cnt+1,0),tmp1+1,999)

作成した2次元配列に歌詞部分のみ取得して格納します。

1行前で変数に入れたタイムタグの位置から先をstrmidで取得しています。

999文字取得していますが、1行にそんなに入れないだろうし、

そもそも一画面に収まる長さでもないと思うので、99文字でも十分だとは思います。

2-3.タイムタグを秒数に変換して配列に入れる
		if instr(kasi(cnt+1,0),0,"[")=0 & (tmp1=9 | tmp1=6) {					;タイムタグの開始([)が1文字目にあり、終了(])が7か10文字目の場合のみ処理
			kasi(cnt+1,0) = strmid(kasi(cnt+1,0),1,tmp1-1)					;タイムタグの時間のみ取得
			m = strmid(kasi(cnt+1,0),0,2) : m=int(m)*60
			s = strmid(kasi(cnt+1,0),3,2)
			if 0<=int(s) & int(s)<=59 : s=int(s) : else : kasi(cnt+1,0) = "" : continue	;秒数が0~59までならその秒数を入れるが、それ以外の場合はタイムタグを空にして処理終了(次の行へ)
			if strmid(kasi(cnt+1,0),5,1)=":" | strmid(kasi(cnt+1,0),5,1)="." {		;コンマ〇秒の表記で「:」か「.」の場合のみ処理、それ以外はコンマ0として扱う
				f = strmid(kasi(cnt+1,0),6,2) : f=double("0."+f)			;コンマ〇秒を取得
			} else : f = 0
			kasi(cnt+1,0) = str(f+s+m)
		} else : kasi(cnt+1,0) = ""								;条件一致しない場合はタイムタグは空欄にする

if instr(kasi(cnt+1,0),0,"[")=0 & (tmp1=9 | tmp1=6) {

まずは簡単ですが、文字数でタイムタグの有無を確認します。

"["が1文字目にあり、尚且つ"]"が7文字目か10文字目にあればタイムタグありと認識。

それ以外の文字数に存在してる場合は勿論、

そもそも""で囲まれてない場合はinstrでは-1が返ってくる為この時点で弾けます。

kasi(cnt+1,0) = strmid(kasi(cnt+1,0),1,tmp1-1)

タイムタグの""を除いた部分"mm:ss:ff"のみ取得します。

開始が1なのは1文字目が"["で確定してるため、2文字目から取得するため。

文字数は"]"が存在する位置から"]"を除くために更に-1してます。

m = strmid(kasi(cnt+1,0),0,2) : m=int(m)*60

s = strmid(kasi(cnt+1,0),3,2)

mに分、sに秒の箇所を取得して入れています。

""は取り除いている為、1~2文字目が分、次の":"を飛ばして

4~5文字目が秒として取得できるという計算です。

また、秒数に変換するため、分(m)については×60しています。

if 0<=int(s) & int(s)<=59 : s=int(s) : else : kasi(cnt+1,0) = "" : continue

ここで秒(s)が60以上でないかをチェックしています。

0~59だった場合は秒(s)を入れた変数を数値に変換。

それ以外だった場合はタイム格納用の2次元配列に空欄を指定して次の行の処理に行きます。

空欄を指定する理由は、実際に歌詞を表示する処理を行う際、

タイムが空欄の場合は次の行を表示するようにする為です。

if strmid(kasi(cnt+1,0),5,1)=":" | strmid(kasi(cnt+1,0),5,1)="." {

f = strmid(kasi(cnt+1,0),6,2) : f=double("0."+f)

} else : f = 0

次はミリ秒の箇所に対応していきます。

""を除いたタイムタグの6文字目(mm:ssの後)が":"か"."となっているか確認します。

TRUEの場合、fにミリ秒を取得して入れます。

":"もしくは"."の後、7~8文字目が該当箇所となるため、そこを取得してます。

また、そのままでは秒数となってしまう為、

"0."を頭に追加することで小数点以下とし、それを実数値に変換しています。

誤ってintにしないよう注意。

ifの結果FALSEだった場合、ミリ秒の指定は無し(=0)としました。

ここを"f=0"ではなく"kasi(cnt+1,0) = "" : continue"とすると、

タイムタグと認識せず、その行の歌詞を表示しない対応にできます。

":"か"."で区切れてない時点でタイムタグとしてはアウトだとは思うので、

そちらの方が良いかなとは思うものの、そこまで厳しくしなくてもいっか、

と思ってミリ秒を0として認識する程度に留めています。

kasi(cnt+1,0) = str(f+s+m)

そして2次元配列のタイムを入れる所にこれまで取得&計算した

秒数を加算して入れていきます。

最初にミリ秒(f)を入れているのは、計算する一番最初の数値が整数の場合、

その後の実数値がすべて整数に変換される仕様があるためです。

加算する順番で計算結果は変わらないため、ミリ秒から加算していってます。

そして"str"で文字列に変換しているのは、配列変数が文字列型の為。

使用時は数値として使用するので、本当は数値のままが良いのですが、

数値のままで入れようとするとエラーが出ます。

} else : kasi(cnt+1,0) = ""

2-3最初に出てきたif命令に一致しなかった場合の処理です。

("[]"でタイムタグがくくられているか確認した行のif命令)

FALSEだった場合はタイムタグは無いものとして扱う為、空欄にしています。

2-4.ループ処理と後片付け
		wait 0		        	            	;フリーズ防止
	loop
	tmp_index = 0 : tmp=0 : tmp1=0 : f=0 : s=0 : m=0	;変数リセット

wait 0はフリーズ防止で入れてます。

無くても動きはしますが、スペック低いPCややたらと文字数や行数が多い場合など、

ループ内の処理に時間がかかる場合、ソフトが「応答なし」となってしまいます。

待てばその内処理が終わって元に戻るものの、

「応答なし」となるとフリーズしてるように見えて焦りますし、

あまりいい状況ではありませんので、その状況を回避する目的で入れてます。

loopはそのままループ。説明不要だと思う。(必要なら公式のヘルプ見て)

最後に、ここまで使用してきたけど必要なくなった変数を全てリセットします。

リセットするとしないとでは、RAM使用量に約0.1MBの差が出ました。

今時のPCのRAM容量を考えると微々たるものですが、

使わないのなら保持してても仕方ないですし。ちりも積もれば山となる、です。

2-5.ここまでのまとめ

2-1~2-4.で行った処理をまとめます。

	notesel tmp
	noteload "歌詞.txt"										;歌詞ファイル読み込み
	kasi_max = notemax										;行数を取得
	sdim kasi,,kasi_max+1,2										;歌詞の行数分配列変数用意
	repeat kasi_max											;歌詞の行数分繰り返す
		getstr kasi(cnt+1,0),tmp,tmp_index							;一行ずつ取り出し
		tmp_index += strsize									;次の行の開始位置を計算
		tmp1 = instr(kasi(cnt+1,0),0,"]")							;タイムタグの最後の場所を入れる
		kasi(cnt+1,1) = strmid(kasi(cnt+1,0),tmp1+1,999)					;歌詞の内容のみを取得
		if instr(kasi(cnt+1,0),0,"[")=0 & (tmp1=9 | tmp1=6) {					;タイムタグの開始([)が1文字目にあり、終了(])が7か10文字目の場合のみ処理
			kasi(cnt+1,0) = strmid(kasi(cnt+1,0),1,tmp1-1)					;タイムタグの時間のみ取得
			m = strmid(kasi(cnt+1,0),0,2) : m=int(m)*60
			s = strmid(kasi(cnt+1,0),3,2)
			if 0<=int(s) & int(s)<=59 : s=int(s) : else : kasi(cnt+1,0) = "" : continue	;秒数が0~59までならその秒数を入れるが、それ以外の場合はタイムタグを空にして処理終了(次の行へ)
			if strmid(kasi(cnt+1,0),5,1)=":" | strmid(kasi(cnt+1,0),5,1)="." {		;コンマ〇秒の表記で「:」か「.」の場合のみ処理、それ以外はコンマ0として扱う
				f = strmid(kasi(cnt+1,0),6,2) : f=double("0."+f)			;コンマ〇秒を取得
			} else : f = 0
			kasi(cnt+1,0) = str(f+s+m)
		} else : kasi(cnt+1,0) = ""								;条件一致しない場合はタイムタグは空欄にする
		wait 0											;フリーズ防止
	loop
	tmp_index = 0 : tmp=0 : tmp1=0 : f=0 : s=0 : m=0						;変数リセット

そのまま繋げただけです。これで歌詞を表示する準備は出来ました。

配列変数に表示する秒数とその歌詞が入ってます。

例えば、この後に

	mes kasi(1,0)	;1行目の歌詞を表示する秒数
	mes kasi(1,1)	;1行目の歌詞の内容

を追加すれば、1行目の歌詞を表示する秒数と歌詞の内容が表示されます。

"kasi(5,1)"にすれば5行目の内容が表示されますし好きな行数の内容を表示できるはずです。

(作成時の要素数を超えると当然ながらエラー発生するので注意)

 

3.楽曲に合わせてタイミングよく歌詞を表示する

3-1.まずは楽曲を再生する
	pos 0,0 : axobj mp,"{6BF52A52-394A-11D3-B153-00C04F79FAA6}",0,0		;WMP配置
	mp("uiMode")="none"							;UI非表示
	mpc = mp("controls")
	mp("URL")="楽曲のパス"							;曲を読み込み
	mpc->"play"								;再生

まずは楽曲を再生します。

再生方法は、現在再生しているタイムが取得できれば何でもOKです。

今回はWindowsMediaPlayer(以下WMP)を使用して再生しています。

「楽曲のパス」には再生する楽曲のパスを指定してください。

3-2.再生しているタイムを取得する
	a=1
	repeat
		redraw 0
		time_now1 = mpc("CurrentPosition")			;現在再生中のタイム取得(秒単位)
		time_now2 = mpc("CurrentPositionString")		;現在再生中のタイム取得
		color 255,255,255 : boxf 0,25,500,74 : color 0,0,0
		pos 5,25 : mes time_now2+" / "+time_now1

ここで「repeat」が出ていますが、「loop」は後程追加します。

この時点で実行してもエラーがでます。

a=1

後程使用する変数の準備です。

現時点で何行目の歌詞を参照しているのか

(次に表示する歌詞が何行目か)格納するために使用します。

実際に使用する際は分かりやすい変数名にした方が良いと思います。

    repeat
        redraw 0

楽曲再生中はここからの処理を繰り返し実行するためrepeatを入れます。

また、画面点滅を防ぐためにredraw 0を使用します。

time_now1 = mpc("CurrentPosition")
time_now2 = mpc("CurrentPositionString")

何方も現在再生しているタイムを取得するものですが、

now1は秒数([10.12345]のような感じ)で、

now2は分:秒([00:10]のような感じ)で変数に入ります。

歌詞表示に使用するのはnow1の方のみですが、分かりやすいようにnow2も取得します。

        color 255,255,255 : boxf 0,25,500,74 : color 0,0,0
        pos 5,25 : mes time_now2+" / "+time_now1

タイムを表示する箇所を背景色(白)で塗りつぶしてリセットした後、

取得した、現在のタイムを表示させます。

歌詞表示とは関係なく、ただ分かりやすいようにするだけの意図なので、

必要なければこの部分は不要です。

(記事最上部にある画像の1行目にある内容を表示してます)

3-3.再生しているタイムが歌詞を表示するタイムを超えた場合歌詞を表示する
		if kasi_max>=a {
			pos 5,50 : mes "NEXT : "+a+" / "+kasi(a,0)
			if kasi(a,0)="" : a+=1 : else {
				if double(kasi(a,0))<=double(time_now1) {
					color 255,255,255 : boxf 0,75,500,100 : color 0,0,0
					pos 5,75 : mes kasi(a,1) : a+=1
				}
			}
		}

if kasi_max>=a {

歌詞の行数(kasi_max)と、次に表示する歌詞の行数(a)を比較しています。

歌詞の行数(kasi_max)を超えた場合はそれ以上表示する歌詞は無いという事なので、

これ以降の処理は行わないようにしています。

pos 5,50 : mes "NEXT : "+a+" / "+kasi(a,0)

次に表示する歌詞の行数(a)と、同じく次に表示する歌詞がいつ表示されるかを表示します。

こちらも分かりやすさの為の意図なので、必要なければこの部分は不要です。

(記事最上部にある画像の2行目にある内容を表示しています)

if kasi(a,0)="" : a+=1 : else {

次に表示する歌詞のタイムが空欄かどうか比較しています。

空欄の場合は表示する歌詞の行数を+1(次行に)してこれ以降の処理は行いません。

タイムタグに問題があったりタイムタグがない行は、

以前の工程でタイムを空欄とするようにしていた為、

この時点ではじく処理を入れることで、上記のような行をスルーして次の行に移れます。

if double(kasi(a,0))<=double(time_now1) {

次に表示する歌詞のタイムと現在再生しているタイムを比較しています。

現在のタイムが歌詞のタイムより大きい(歌詞のタイムを過ぎた)場合に、

この後の歌詞を表示する処理を行います。

                    color 255,255,255 : boxf 0,75,500,100 : color 0,0,0
                    pos 5,75 : mes kasi(a,1) : a+=1

歌詞を表示する場所を背景色(白)で塗りつぶしてリセットし、歌詞の内容を表示させます。

また、変数「a」を+1し、表示する行数を次の行にしておきます。

                }
            }
        }

言うまでもないのですが、これまで使用したif命令の括弧を閉じます。

3-4.表示反映やループ処理をする
		redraw 1
		wait 1
	loop

ここまでの内容を反映するために「redraw 1」命令を使用し、

フリーズ防止等の目的で「wait 1」を入れ、

最後に「repeat」をするために「loop」を入れます。

3-5.ここまでのまとめ

3-1~3-4で行った処理をまとめます。

	pos 0,0 : axobj mp,"{6BF52A52-394A-11D3-B153-00C04F79FAA6}",0,0		;WMP配置
	mp("uiMode")="none"							;UI非表示
	mpc = mp("controls")
	mp("URL")="楽曲のパス"							;曲を読み込み
	mpc->"play"								;再生
	
	a=1
	repeat
		redraw 0
		time_now1 = mpc("CurrentPosition")				;現在再生中のタイム取得(秒単位)
		time_now2 = mpc("CurrentPositionString")			;現在再生中のタイム取得
		color 255,255,255 : boxf 0,25,500,74 : color 0,0,0
		pos 5,25 : mes time_now2+" / "+time_now1
	
		if kasi_max>=a {
			pos 5,50 : mes "NEXT : "+a+" / "+kasi(a,0)
			if kasi(a,0)="" : a+=1 : else {
				if double(kasi(a,0))<=double(time_now1) {
					color 255,255,255 : boxf 0,75,500,100 : color 0,0,0
					pos 5,75 : mes kasi(a,1) : a+=1
				}
			}
		}
		redraw 1
		wait 1
	loop

これを2.の項目までで行った処理とつなげれば完成です。

 

4.最後に

このコードで、普通に再生し、歌詞を表示することは可能になりました。

が、問題は残っており、

シークバー等を使用して楽曲の再生位置を戻したりした場合、

歌詞の表示はその地点に戻らず、変更前の状態のままとなるなど、

実際に使用するには改造が必要なので注意してください。