VBAでAIFFのサンプリングレートなどを取得【備忘録】

前回のWAVのサンプリングレートなどを取得するスクリプトの続きで、今回はAIFFです。


AIFFはWAVと違って情報が少なく、また少し変わっていて苦労しました。情報というか、あれこれ検証してみて恐らくこうなってるんだろうなといった感じですね。なので、一通りは手持ちのAIFFファイルで動作確認して問題はなくなりましたが、思わぬ不具合もあるかもしれません。特にAIFFはWAVに比べると手持ちのファイルも少なく、その可能性は充分考えられます。


VBScriptは前回の部分と違う部分、VBAはReadBinary関数とGetChunkPos関数以外を掲載します。

VBAで取得


拡張子で判定するところをAIFF関連にします。またパスを格納する変数の名前がaifPathArrに変わっているので、全て変えます。記載してませんが、呼び出すマクロ名も変更です。

' GetAiffInfo.vbs
Select Case UCase(fs.GetExtensionName(arg))
Case "AIFF", "AIF", "AIFC", "AFC", "SND"
	push aifPathArr, arg
	cnt = cnt + 1
End Select


以下VBA。本文は微妙に変わってます。

' GetAiffInfo.xlsm
Option Explicit

Sub GetAiffInfo(aifPathArr As Variant)

	Dim srate, bits, r, cnt, ckPos
	Dim bin
	Dim ch
	Dim arr, a
	Dim aifPath, aifFname

	r = 2
	For Each aifPath In aifPathArr

		aifFname = Mid(aifPath, InStrRev(aifPath, "\") + 1)
		bin = ReadBinary(aifPath, 459)

		If IsAiff(bin) Then

			srate = 0
			bits = 0
			ch = ""

			ckPos = GetChunkPos(bin, Array(&H43, &H4F, &H4D, &H4D))
			If ckPos Then
				srate = GetAiffSamplingRate(bin, ckPos)
				bits = GetAiffBitRate(bin, ckPos, IsAifc(bin))
				ch = GetAiffChannel(bin, ckPos)
			Else
				MsgBox aifFname & " に COMM の文字列がありません。", vbExclamation
			End If
			arr = Array(aifFname, CStr(srate / 1000) & " kHz", CStr(bits) & " bit", ch)
			cnt = 1
			For Each a In arr
				Cells(r, cnt).Value = a
				cnt = cnt + 1
			Next
			r = r + 1

		End If

	Next

	If r > 2 Then Columns(1).AutoFit

End Sub

Function IsAiff(bin)
	If 11 <= UBound(bin) Then
		If bin(0) = &H46 And bin(1) = &H4F And bin(2) = &H52 And bin(3) = &H4D And bin(8) = &H41 And bin(9) = &H49 And bin(10) = &H46 Then
			Select Case bin(11)
			Case &H46, &H43
				IsAiff = True
				Exit Function
			End Select
		End If
	End If
	IsAiff = False
End Function

Function IsAifc(bin)
	If 11 <= UBound(bin) Then
		If bin(0) = &H46 And bin(1) = &H4F And bin(2) = &H52 And bin(3) = &H4D And bin(8) = &H41 And bin(9) = &H49 And bin(10) = &H46 And bin(11) = &H43 Then
			IsAifc = True
			Exit Function
		End If
	End If
	IsAifc = False
End Function

Function GetAiffChannel(bin, ckPos)
	Dim cnPos
	cnPos = ckPos + 9
	If cnPos <= UBound(bin) Then
		If bin(cnPos) = 2 Then
			GetAiffChannel = "ステレオ"
		Else
			GetAiffChannel = "モノラル"
		End If
		Exit Function
	End If
	GetAiffChannel = ""
End Function

Function GetAiffBitRate(bin, ByVal ckPos, ByVal is_aifc)
	Dim flPos, brPos
	brPos = ckPos + 15
	flPos = brPos + 11
	If flPos + 3 <= UBound(bin) Then
		If is_aifc And (bin(flPos) = &H66 And bin(flPos + 1) = &H6C) Then
			If bin(flPos + 2) = &H33 And bin(flPos + 3) = &H32 Then
				GetAiffBitRate = 32
				Exit Function
			ElseIf bin(flPos + 2) = &H36 And bin(flPos + 3) = &H34 Then
				GetAiffBitRate = 64
				Exit Function
			End If
		End If
	End If
	GetAiffBitRate = bin(brPos)
End Function

Function GetAiffSamplingRate(bin, ckPos)
	Dim srPos, x
	srPos = ckPos + 17
	If srPos + 2 <= UBound(bin) Then
		x = 2 ^ (bin(srPos) - &HE)
		GetAiffSamplingRate = x * (bin(srPos + 1) * &H100 + bin(srPos + 2))
		Exit Function
	End If
	GetAiffSamplingRate = 0
End Function
COMMチャンクの位置・Limit値


WAVデータに比べAIFFはCOMMチャンクがずれているデータは少ない印象でした。はじめはチャンクの位置を取得せず頭からの位置で取得するよう書いていた位なので。LogicのAIFFのチャンクの位置は430で、そのため459と指定しています。WAVの時のPro Tools程ではないけど、結構ずれてますね。因みにPro ToolsのAIFFはずれていません。

IsAiff関数


AIFFかどうかを判定します。初めの4文字の「FORM」とその少し後ろにある「AIFF」の文字で判定しています。但しAIFCもAIFFとして扱うので、AIFCでもTrueになります。

IsAifc関数


AIFCかどうかを判定します。初めの4文字の「FORM」とその少し後ろにある「AIFC」の文字で判定しています。後のビットレート取得の際にもこれを利用します。

GetAiffChannel関数


ステレオかモノラルかを文字列で返します。位置がAIFF独自の位置に変わっているだけで、WAVと全く同じ仕様です。

GetAiffBitRate関数


ビットレートを取得します。基本的には前項チャンネルと同じでWAVと同じ仕様なのですが、CubaseのWAVの謎仕様に対応しています。


Cubaseで書き出された32 Bit Floatファイルにてビットレートを取得すると、16 Bitと表示されることに気付きました。なぬっと思ってバイナリを見てみても、確かに10(10進数16)となっています。WAVの32 Bit Floatでは問題なかったのに。どうやらこれはCubase独自の仕様のようで、Pro Toolsや他のソフトの32 Bit Floatでは正しい値が入っていました。



仕方がないため、32 Bit Floatファイルにはfl32とかそれを表す文字列が幾つかあったので、それが含まれていたら32 Bitとすることにしました。尚、AIFCであるかも条件分岐に含まれていますが、AIFFの規格上浮動小数点はAIFCとなるようです。なのでAIFCの場合のみこの処理を行います。


自分の環境では64 Bit Floatのデータを作ることは出来ず、そもそも64 Bitがあること自体知らなかったのですが、AIFFの情報を探している時に色んなパターンのAIFFのサンプルデータを配布しているサイトがあり、そこに64 Bit Floatもあったので、64 Bitの処理も一応合わせて加えました。まぁもっとも、Cubaseで生成されたものではないので、本来の位置に40(10進数64)は入っているのですが。


32 BitもFloatしか聞いたことがなかったため、Floatだけ存在すると思ってたのですが、Int型もあるんですね。当然と言えば当然かもしれませんが、Int型の32 Bitファイルにはfl32などの文字列はありませんでした。

GetAiffSamplingRate関数


前項は一部のファイルで特殊でしたが、サンプリングレートも特殊です。こちらはAIFFの仕様上特殊な感じです。ちょっと長くなるかもしれません。


はじめは例の知恵袋のサンプルと同様の感じで、但しビットシフトの数は訂正して処理を走らせてみました。しかし全く的外れな値が返ってくるんですね。バイナリエディタで見てみると、それらしい値はサンプルでの位置の1バイト後ろから始まっており、しかもWAVデータで言うと1バイト目と2バイト目がひっくり返っているような状態でした。つまりWAVで言う2バイト目ではなく1バイト目にビットシフトの掛け算を行うことで、正しい数字が返ります。



例のサンプルは色々間違いがあったため、1バイトずれてるのも間違いだったんだなとこの時点では思い、1バイトずらしました。ですが、後程ハイレートのデータで正しく取得できないことに気付きました。但し全く正しくない感じではなく、例えば88.2kHzなら44.1kHzと表示されたり、96kHzなら48kHzと表示されたり、規則性はありました。


WAVのようにハイレートになると後ろのバイトを使うといった挙動もなく、結局1バイトずらしたけど、やっぱり1バイト目は何かで必要なんだということに行き着き、位置は元に戻しました。44.1kHzと88.2kHzでは、1バイト目の数字に差が出来ることに気付きました。そしてこの差を利用して算出しているのでは、と仮説を立てました。この場合差は1だったのですが、例えば1なら2倍になる、みたいな感じです。


そこで今まで1度も書き出したことのなかった176.4kHzや192kHzを書き出してもっと検証してみることにしました。するとこれらでは差が2つあることが分かりました。2であると4倍にする、と考えると、つまり2の差分乗である可能性が出てきます。ただこの時点ではこう断定するには差分が足りず、Select Case文で個別に条件分岐して処理してました。


差が増えるとどうなるのか知りたかったので、上方向はCubaseの仕様上打ち止めなので下方向も書き出してみることにしました。因みにこれらも今まで1度も書き出したことはありませんでした。すると一番音質の悪い8kHzでは、基準の周波数に0.125を掛ける(つまり2 ^ -3、1/8)と8000が返ることが分かりました。そしてその他の周波数も検証して正しい値が返るよう x を調整していくと、以下のようなSelect Case文が出来ました。

Select Case bin(i)
	Case &HB: x = 0.125 '10進数11 差分-3
	Case &HC: x = 0.25 '10進数12 差分-2
	Case &HD: x = 0.5 '10進数13 差分-1
	Case &HE: x = 1 '10進数14 差分0 基準
	Case &HF: x = 2 '10進数15 差分1
	Case &H10: x = 4 '10進数16 差分2
	Case Else: x = 0
End Select


やはり仮説通り2の差分乗になっているようですね。よって、この時点でSelect Caseを止めて「x = 2 ^ (bin(srPos) - &HE)」と置き換えました。&HE(10進数14)が基準となっており、バイナリに入ってる値からこの基準値を引いて出来た値分2を累乗します。もし基準と同じ値が来たら、0なので2 ^ 0をすると1が返り、掛け算をしても変わりません。


結局1バイト目を上記の掛け算用、2バイト目と3バイト目を使って基準のサンプリングレートを算出している仕様なので、関数も3バイトだけで算出しています。面白い仕様で、効率的な仕様だなと思います。


以上のような仕様のためか、情報を探している時に、AIFFのサンプリングレートの仕様がよく分からないといった声がちらほらありました。


追記

GetAiffBitRate関数(変更)


元の関数のFloat部分は32 Bitと64 Bitに限定されたものでしたが、どんな数字が来ても大丈夫なように数字を直接入れる仕様にしました。


処理としては、fl32やfl64の3バイト目から、1バイトずつバイナリを文字列に変換し、数字の文字列でなくなるまでループして文字列を繋ぎ合わせます。そして結果出来た数字文字列を数字に変換して返します。


数字が何文字も続くことはあり得ないので、For文で指定のリミット数まで回すように書いて、途中で数字じゃなくなったらExit Forでも良かったのですが。


あと、多分大丈夫だと思うけど、現状数字文字列以外で区切られてるので良いですが、例えばfl32に64が続いてfl3264とか変な表現があったらそれをそのまま取得してしまうので、どうしようかなとも思ったけどそこまで気にしなくていいか。でも数字文字列が続く限りと処理したので、fl8やfl128とか出てきても対応出来るのは良くなったと思います。それか現実的な数字を予め登録しておいてそれに該当したら返す形でも良いかも。

Function GetAiffBitRate(bin, ByVal ckPos, ByVal is_aifc)
	Dim flPos, brPos, nPos
	Dim strFlNum
	brPos = ckPos + 15
	flPos = brPos + 11
	If flPos + 2 <= UBound(bin) Then
		If is_aifc And (bin(flPos) = &H66 And bin(flPos + 1) = &H6C) Then
			nPos = flPos + 2
			Do While IsNumeric(Chr(bin(nPos)))
				strFlNum = strFlNum & Chr(bin(nPos))
				nPos = nPos + 1
				If nPos > UBound(bin) Then Exit Do
			Loop
			If strFlNum <> "" Then
				GetAiffBitRate = CLng(strFlNum)
				Exit Function
			End If
		End If
	End If
	GetAiffBitRate = bin(brPos)
End Function

WAV、AIFF統合版


AIFF版が出来た時点で分けている意味がなかったので、統合しました。個人的には仕事上で非圧縮のWAVとAIFFの値さえ取得出来れば良いのですが、MP3とかも足していくかもしれません。


VBScriptは拡張子を統合して変数名をオーディオファイル全般を指す名前に変更。呼び出すマクロ名も変更。

' GetAudioInfo.vbs
Select Case UCase(fs.GetExtensionName(arg))
Case "WAV", "AIFF", "AIF", "AIFC", "AFC", "SND"
	push audioPathArr, arg
	cnt = cnt + 1
End Select


関数はこれまでと同じなので省略してます。但し、ReadBinary以外の関数は全てクラスモジュールに移動させました。変数名cAudioから関数を呼び出しています。

' GetAudioInfo.xlsm
Option Explicit

Sub GetAudioInfo(audioPathArr As Variant)

	Dim srate, bits, r, cnt, ckPos
	Dim bin
	Dim ch, audioPath, audioFname
	Dim arr, a
	Dim is_wave, is_aiff, is_aifc
	Dim cAudio As New AudioClass
	Dim fs

	Set fs = CreateObject("Scripting.FileSystemObject")

	r = 2
	For Each audioPath In audioPathArr
		Select Case UCase(fs.GetExtensionName(audioPath))
		Case "AIFF", "AIF", "AIFC", "AFC", "SND"
			bin = ReadBinary(audioPath, 459)
		Case Else
			bin = ReadBinary(audioPath, 745)
		End Select

		With cAudio
			is_wave = .IsWave(bin)
			is_aiff = .IsAiff(bin)
			is_aifc = .IsAifc(bin)
			audioFname = fs.GetFileName(audioPath)

			If is_wave Or is_aiff Then

				srate = 0
				bits = 0
				ch = ""

				If is_wave Then
					ckPos = .GetChunkPos(bin, Array(&H66, &H6D, &H74, &H20))
					If ckPos Then
						srate = .GetWaveSamplingRate(bin, ckPos)
						bits = .GetWaveBitRate(bin, ckPos)
						ch = .GetWaveChannel(bin, ckPos)
					Else
						MsgBox audioFname & " に fmt の文字列がありません。", vbExclamation
					End If
				ElseIf is_aiff Then
					ckPos = .GetChunkPos(bin, Array(&H43, &H4F, &H4D, &H4D))
					If ckPos Then
						srate = .GetAiffSamplingRate(bin, ckPos)
						bits = .GetAiffBitRate(bin, ckPos, is_aifc)
						ch = .GetAiffChannel(bin, ckPos)
					Else
						MsgBox audioFname & " に COMM の文字列がありません。", vbExclamation
					End If
				End If

				arr = Array(audioFname, CStr(srate / 1000) & " kHz", CStr(bits) & " bit", ch)
				cnt = 1
				For Each a In arr
					Cells(r, cnt).Value = a
					cnt = cnt + 1
				Next
				r = r + 1

			End If
		End With

	Next

	Set fs = Nothing

	If r > 2 Then Columns(1).AutoFit

End Sub


WAV版はこちら