SMFを読み取るクラス
SMF(Standard MIDI File)を読み取るクラスがほしくなった。用途はピアノ練習ソフトのため。コントロールチェンジとかチャネルとかは基本的に無視。ノートが取れればよい。
ネットを検索すると、Cで書かれたリーダーは結構あるけど、vb.net とかで書かれたソースは発見できず。車輪の再発明をすることにした。SMFにはFormat0/Format1/Format2の3タイプがあるらしいけど、SMF2はマイナーなので無視することにする。あと、データの格納がなぜかビックエンディアンなのでちょっといらっとしますが、もりもり書いてみた。
SMFの仕様については以下のサイト等を参考にしてみた。書いてくれた人サンクス!
http://www.geocities.co.jp/SiliconValley-SanJose/8132/
'// SMFを読み取る dim reader as SmfReader = SmfReader.Read("c:\test.mid") '// トラック0の最初のイベントを取得する reader.Track(0).Events(0)
こんな感じでノートを取得できるような感じ。一応メタイベントとかも適当に格納してある。
ソースは以下から
Public Class SmfReader Public Class SmfException Inherits ApplicationException Public Sub New() MyBase.New() End Sub Public Sub New(ByVal message As String) MyBase.New(message) End Sub Public Sub New(ByVal message As String, ByVal innerException As Exception) MyBase.New(message, innerException) End Sub End Class Private m_header As SmfHeader Private m_tracks As New List(Of SmfTrack) ''' <summary> ''' SMFのヘッダ情報を取得します。 ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> Public Property Header() As SmfHeader Get Return m_header End Get Set(ByVal value As SmfHeader) m_header = value End Set End Property ''' <summary> ''' トラック情報を取得します。 ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> Public Property Tracks() As List(Of SmfTrack) Get Return m_tracks End Get Set(ByVal value As List(Of SmfTrack)) m_tracks = value End Set End Property '// こんな感じの階層を作るよ '// SmfReader '// -Header '// -Tracks '// -Track0 '// -Events '// -MidiEvent '// -SysExEvent '// -MetaEvent '// -Track1 '// -Events '// -MidiEvent '// -SysExEvent '// -MetaEvent Public Enum MetaEventType V00シーケンス番号 = 0 V01テキスト = 1 V02著作権表示 = 2 V03トラック名 = 3 V04楽器名 = 4 V05歌詞 = 5 V06マーカ = 6 V07キューポイント = 7 V20MIDIチャンネルプリフィックス = 32 V2Fエンドオブトラック = 47 V51セットテンポ = 81 V54SMPTEオフセット = 84 V58拍子 = 88 V59調号 = 89 V7Fシーケンサ固有のメタイベント = 127 End Enum Public Enum MidiEventType V8xノートオフ V9xノートオン VAxポリフォニック・キープレッシャー VBxコントロールチェンジ VCxプログラム・チェンジ VDxチャネル・プレッシャー VExピッチベンド・チェンジ VXxUnknown End Enum ''' <summary> ''' SMFファイルを読み込みます。 ''' </summary> ''' <param name="path">ファイルパス</param> ''' <returns></returns> ''' <remarks></remarks> Public Shared Function ReadData(ByVal path As String) As SmfReader Dim reader As New SmfReader Dim bytes() As Byte = System.IO.File.ReadAllBytes(path) Dim data As String = ToHexString(bytes) '// バイト配列をちょん切ったりなんかするからListのほうが便利 '// 読み取ったバイトは削除していくので現在読み取っている場所はindex0を読み取ればよし Dim bytesList As New List(Of Byte)(bytes) Try '// MThd ヘッダを探す If Not data.StartsWith("4D546864") Then Throw New SmfException("SMFファイルのヘッダが不正です。MThdで開始していません。") End If '// ヘッダ情報を作成 reader.m_header = CreateHeader(bytesList) For i As Integer = 0 To reader.m_header.TrackCount - 1 Dim trackString As String = ToHexString(bytesList.ToArray) '// トラックを探して、ヘッダ情報をちょん切ったByte配列を作成 bytesList.RemoveRange(0, CInt(trackString.IndexOf("4D54726B") / 2)) reader.m_tracks.Add(CreateTrack(bytesList)) Next If reader.Tracks.Count <= 0 Then Throw New SmfException("SMFファイルのトラック情報が不正です。トラックが見つかりません。") End If Catch ex As Exception Throw New SmfException("SMFの読み込み中にエラーが発生しました。", ex) End Try Return reader End Function ''' <summary> ''' ヘッダを作成します。 ''' </summary> ''' <param name="headerByte"></param> ''' <returns></returns> ''' <remarks></remarks> Private Shared Function CreateHeader(ByVal headerByte As List(Of Byte)) As SmfHeader Dim header As New SmfHeader '// Format Select Case ReverseToInt16(headerByte, 8) Case 0 header.Format = "SMF0" header.FormatValue = 0 Case 1 header.Format = "SMF1" header.FormatValue = 1 Case Else Throw New SmfException("対応していないSMFフォーマットです。SMF0かSMF1のみ対応しています。") End Select '// トラック数 header.TrackCount = ReverseToInt16(headerByte, 10) '// 時間単位(分解能) header.TimeBase = ReverseToInt16(headerByte, 12) Return header End Function ''' <summary> ''' トラックを作成します。 ''' </summary> ''' <param name="trackByte"></param> ''' <returns></returns> ''' <remarks></remarks> Private Shared Function CreateTrack(ByVal trackByte As List(Of Byte)) As SmfTrack Dim track As New SmfTrack '// データ長を読み取って読み取り済みのところは捨てる track.DataLength = ReverseToInt32(trackByte, 4) trackByte.RemoveRange(0, 8) '// トラックが終わるであろう位置を計算 Dim endTrackIndex As Integer = trackByte.Count - track.DataLength Do While trackByte.Count > endTrackIndex Dim delta As Integer = ReadDeltaTime(trackByte) Dim smfEvent As SmfEvent Select Case trackByte(0) Case 240, 247 '// SysExイベント smfEvent = ReadSysExEvent(trackByte) Case 255 '// メタイベント Dim metaEvent As SmfMetaEvent = ReadMetaEvent(trackByte) '// タイトルを読み込んだのならタイトルに設定(一番最初のトラック名だけ採用) If metaEvent.DataType = MetaEventType.V03トラック名 Then If track.Title.Length <= 0 Then track.Title = metaEvent.Text End If End If smfEvent = metaEvent Case Else '// Midiイベント smfEvent = ReadMidiEvent(trackByte) End Select smfEvent.DeltaTime = delta track.Events.Add(smfEvent) Loop Return track End Function ''' <summary> ''' 与えられたByteからDeltaTimeを読み取って返します。読み取った部分元のリストから削除されます。 ''' </summary> ''' <param name="bytes"></param> ''' <returns></returns> ''' <remarks></remarks> Private Shared Function ReadDeltaTime(ByVal bytes As List(Of Byte)) As Integer Dim result As Integer If bytes(0) <= &H7F Then '// 最初のByteの最上位Bitが立っていないのでDeltaTimeは1バイト result = bytes(0) bytes.RemoveRange(0, 1) ElseIf bytes(1) <= &H7F Then '// 2Byte目の最上位Bitが立っていないのでDeltaTimeは2バイト result = (bytes(0) And &H7F) * &H80 result = (result Or (bytes(1) And &H7F)) bytes.RemoveRange(0, 2) ElseIf bytes(2) <= &H7F Then '// 3Byte目の最上位Bitが立っていないのでDeltaTimeは3バイト result = (bytes(0) And &H7F) * &H80 result = (result Or (bytes(1) And &H7F)) * &H80 result = (result Or (bytes(2) And &H7F)) bytes.RemoveRange(0, 3) ElseIf bytes(3) <= &H7F Then '// 4Byte目の最上位Bitが立っていないのでDeltaTimeは4バイト result = (bytes(0) And &H7F) * &H80 result = (result Or (bytes(1) And &H7F)) * &H80 result = (result Or (bytes(2) And &H7F)) * &H80 result = (result Or (bytes(3) And &H7F)) bytes.RemoveRange(0, 4) End If Return result End Function ''' <summary> ''' Midiを読み取ります。 ''' </summary> ''' <param name="bytes"></param> ''' <returns></returns> ''' <remarks></remarks> Private Shared Function ReadMidiEvent(ByVal bytes As List(Of Byte)) As SmfMidiEvent If bytes.Count < 2 Then Throw New SmfException("MidiEventの読み込みに失敗しました。長さが足りません。") End If Dim midiEvent As New SmfMidiEvent Static runningStatus As Byte = bytes(0) Static runningDataType As MidiEventType '// チャネルステータスの判定 Select Case bytes(0) And &HF0 Case &H80 '// ノートオフ midiEvent.Channel = bytes(0) And &HF midiEvent.DataType = MidiEventType.V8xノートオフ bytes.RemoveRange(0, 1) Case &H90 '// ノートオン midiEvent.Channel = bytes(0) And &HF midiEvent.DataType = MidiEventType.V9xノートオン bytes.RemoveRange(0, 1) Case &HA0 '// ポリフォニック・キープレッシャー midiEvent.Channel = bytes(0) And &HF midiEvent.DataType = MidiEventType.VAxポリフォニック・キープレッシャー runningDataType = midiEvent.DataType bytes.RemoveRange(0, 1) Case &HB0 '// コントロールチェンジ midiEvent.Channel = bytes(0) And &HF midiEvent.DataType = MidiEventType.VBxコントロールチェンジ bytes.RemoveRange(0, 1) Case &HC0 '// プログラム・チェンジ midiEvent.Channel = bytes(0) And &HF midiEvent.DataType = MidiEventType.VCxプログラム・チェンジ bytes.RemoveRange(0, 1) Case &HD0 '// チャネル・プレッシャー midiEvent.Channel = bytes(0) And &HF midiEvent.DataType = MidiEventType.VDxチャネル・プレッシャー runningDataType = midiEvent.DataType bytes.RemoveRange(0, 1) Case &HE0 '// ピッチベンド・チェンジ midiEvent.Channel = bytes(0) And &HF midiEvent.DataType = MidiEventType.VExピッチベンド・チェンジ bytes.RemoveRange(0, 1) Case Else If bytes(0) <= &H7F Then '// 最上位Bitに1が立っていないのでこれはランニングステータス midiEvent.Channel = runningStatus And &HF midiEvent.DataType = runningDataType '// ここは1バイト取り除く必要はない Else '// 未知のステータス・・・ midiEvent.Channel = bytes(0) And &HF midiEvent.DataType = MidiEventType.VXxUnknown bytes.RemoveRange(0, 1) End If End Select runningDataType = midiEvent.DataType '// ノートとvelocityを取得 midiEvent.Note = bytes(0) bytes.RemoveRange(0, 1) '// プログラム・チェンジとチャネル・プレッシャー以外は2バイト目も取得 Select Case midiEvent.DataType Case MidiEventType.VCxプログラム・チェンジ, MidiEventType.VDxチャネル・プレッシャー Case Else midiEvent.Velocity = bytes(0) bytes.RemoveRange(0, 1) End Select '// velocity 0 のノートオンはノートオフに置き換える If midiEvent.DataType = MidiEventType.V9xノートオン Then If midiEvent.Velocity = 0 Then midiEvent.DataType = MidiEventType.V8xノートオフ End If End If Return midiEvent End Function ''' <summary> ''' メタイベントを読み取ります。 ''' </summary> ''' <param name="bytes"></param> ''' <returns></returns> ''' <remarks></remarks> Private Shared Function ReadMetaEvent(ByVal bytes As List(Of Byte)) As SmfMetaEvent If bytes.Count < 3 Then Throw New SmfException("メタデータの読み込みに失敗しました。長さが足りません。") End If '// タイプとデータ長を読み取って消す Dim metaEvent As New SmfMetaEvent metaEvent.DataType = CType(bytes(1), MetaEventType) metaEvent.DataLength = bytes(2) bytes.RemoveRange(0, 3) '// データを読み取って消す Dim dataBytes As New List(Of Byte) For i As Integer = 0 To metaEvent.DataLength - 1 dataBytes.Add(bytes(i)) Next bytes.RemoveRange(0, metaEvent.DataLength) metaEvent.OriginalData = dataBytes.ToArray Select Case metaEvent.DataType Case MetaEventType.V00シーケンス番号 Case MetaEventType.V01テキスト metaEvent.Text = System.Text.Encoding.Default.GetString(dataBytes.ToArray, 0, metaEvent.DataLength) Case MetaEventType.V02著作権表示 metaEvent.Text = System.Text.Encoding.Default.GetString(dataBytes.ToArray, 0, metaEvent.DataLength) Case MetaEventType.V03トラック名 metaEvent.Text = System.Text.Encoding.Default.GetString(dataBytes.ToArray, 0, metaEvent.DataLength) Case MetaEventType.V04楽器名 metaEvent.Text = System.Text.Encoding.Default.GetString(dataBytes.ToArray, 0, metaEvent.DataLength) Case MetaEventType.V05歌詞 metaEvent.Text = System.Text.Encoding.Default.GetString(dataBytes.ToArray, 0, metaEvent.DataLength) Case MetaEventType.V06マーカ metaEvent.Text = System.Text.Encoding.Default.GetString(dataBytes.ToArray, 0, metaEvent.DataLength) Case MetaEventType.V07キューポイント metaEvent.Text = System.Text.Encoding.Default.GetString(dataBytes.ToArray, 0, metaEvent.DataLength) Case MetaEventType.V20MIDIチャンネルプリフィックス Case MetaEventType.V2Fエンドオブトラック 'トラックの終了 Case MetaEventType.V51セットテンポ '4分音符の長さをμsec単位で表し、一拍当たりの時間でテンポを表す。 metaEvent.DataValue = ReverseToInt32Ex(dataBytes, 0) Case MetaEventType.V54SMPTEオフセット Case MetaEventType.V58拍子 '[nn:dd:cc:bb] '拍子記号の分子nn '2のdd乗で表される分母(dが2の場合4、3の場合8) 'メトロノーム1カウントあたりのMIDIクロック数cc '4分音符中の32分音符数bb Case MetaEventType.V59調号 '[sf:mi] 'シャープまたはフラット記号の数を表すsf 'メジャー/マイナーを示すmi 'sfはフラットの数を表すときはマイナス数値になる。 'また、miはメジャーのとき0、マイナのとき1になる。 Case MetaEventType.V7Fシーケンサ固有のメタイベント End Select Return metaEvent End Function ''' <summary> ''' システムエクスクルーシブイベントを読み取ります。 ''' </summary> ''' <param name="bytes"></param> ''' <returns></returns> ''' <remarks></remarks> Private Shared Function ReadSysExEvent(ByVal bytes As List(Of Byte)) As SmfSysExEvent Dim sysExEvent As New SmfSysExEvent bytes.RemoveRange(0, 1) sysExEvent.DataLength = ReadDeltaTime(bytes) '// データを読み取る Dim dataBytes As New List(Of Byte) For i As Integer = 0 To sysExEvent.DataLength - 1 dataBytes.Add(bytes(i)) Next bytes.RemoveRange(0, sysExEvent.DataLength) '// メッセージを格納 sysExEvent.OriginalData = dataBytes.ToArray Return sysExEvent End Function Private Shared Function ReverseToInt16(ByVal bytes As List(Of Byte), ByVal startIndex As Integer) As Short '// ビックエンディアンでInt16を作成 If bytes.Count < startIndex + 1 Then Throw New SmfException("指定されたByte配列はInt16を作成するのに十分な長さがありません。") End If Dim reverseBytes(1) As Byte reverseBytes(0) = bytes(startIndex + 1) reverseBytes(1) = bytes(startIndex) Return BitConverter.ToInt16(reverseBytes, 0) End Function Private Shared Function ReverseToInt32(ByVal bytes As List(Of Byte), ByVal startIndex As Integer) As Integer '// ビックエンディアンでInt16を作成 If bytes.Count < startIndex + 3 Then Throw New SmfException("指定されたByte配列はInt32を作成するのに十分な長さがありません。") End If Dim reverseBytes(3) As Byte reverseBytes(0) = bytes(startIndex + 3) reverseBytes(1) = bytes(startIndex + 2) reverseBytes(2) = bytes(startIndex + 1) reverseBytes(3) = bytes(startIndex) Return BitConverter.ToInt32(reverseBytes, 0) End Function Private Shared Function ReverseToInt32Ex(ByVal bytes As List(Of Byte), ByVal startIndex As Integer) As Integer '// ビックエンディアンでInt16を作成 If bytes.Count < startIndex + 2 Then Throw New SmfException("指定されたByte配列はInt32を作成するのに十分な長さがありません。") End If bytes.Insert(0, 0) Dim reverseBytes(3) As Byte reverseBytes(0) = bytes(startIndex + 3) reverseBytes(1) = bytes(startIndex + 2) reverseBytes(2) = bytes(startIndex + 1) reverseBytes(3) = bytes(startIndex) bytes.RemoveAt(0) Return BitConverter.ToInt32(reverseBytes, 0) End Function ''' <summary> ''' バイト配列を16進文字列に変換します。 ''' </summary> ''' <param name="target"></param> ''' <returns></returns> ''' <remarks></remarks> Private Shared Function ToHexString(ByVal target() As Byte) As String '// 自分でごにゃごにゃするより、BitConverterのが速い・・ Return BitConverter.ToString(target).Replace("-"c, "") End Function End Class Public Class SmfHeader Private m_formatValue As Integer Private m_format As String Private m_trackCount As Integer Private m_timeBase As Integer ''' <summary>SMFのフォーマットを取得します。</summary> Public Property Format() As String Get Return m_format End Get Set(ByVal value As String) m_format = value End Set End Property ''' <summary>SMFのフォーマット番号を取得します。</summary> Public Property FormatValue() As Integer Get Return m_formatValue End Get Set(ByVal value As Integer) m_formatValue = value End Set End Property ''' <summary>トラック数を取得します。</summary> Public Property TrackCount() As Integer Get Return m_trackCount End Get Set(ByVal value As Integer) m_trackCount = value End Set End Property ''' <summary>時間単位(分解能)を取得します。4分音符の長さを何分割するかの数値</summary> Public Property TimeBase() As Integer Get Return m_timeBase End Get Set(ByVal value As Integer) m_timeBase = value End Set End Property End Class Public Class SmfTrack Private m_dataLength As Integer Private m_events As New List(Of SmfEvent) Private m_title As String = "" ''' <summary>タイトルを取得します。</summary> Public Property Title() As String Get Return m_title End Get Set(ByVal value As String) m_title = value End Set End Property ''' <summary>データ長を取得します。</summary> Public Property DataLength() As Integer Get Return m_dataLength End Get Set(ByVal value As Integer) m_dataLength = value End Set End Property ''' <summary>イベント情報を取得します。</summary> Public Property Events() As List(Of SmfEvent) Get Return m_events End Get Set(ByVal value As List(Of SmfEvent)) m_events = value End Set End Property End Class ''' <summary> ''' Eventの基底クラス ''' </summary> ''' <remarks></remarks> Public Class SmfEvent Protected m_deltaTime As Integer ''' <summary>デルタタイムを取得します。</summary> Public Property DeltaTime() As Integer Get Return m_deltaTime End Get Set(ByVal value As Integer) m_deltaTime = value End Set End Property End Class ''' <summary> ''' MidiEvent ''' </summary> ''' <remarks></remarks> Public Class SmfMidiEvent Inherits SmfEvent Private m_dataType As SmfReader.MidiEventType Private m_channel As Integer Private m_note As Integer Private m_velocity As Integer ''' <summary>データ種別を取得します。</summary> Public Property DataType() As SmfReader.MidiEventType Get Return m_dataType End Get Set(ByVal value As SmfReader.MidiEventType) m_dataType = value End Set End Property ''' <summary>チャンネルを取得します。</summary> Public Property Channel() As Integer Get Return m_channel End Get Set(ByVal value As Integer) m_channel = value End Set End Property ''' <summary>ノート(音階)を取得します。</summary> Public Property Note() As Integer Get Return m_note End Get Set(ByVal value As Integer) m_note = value End Set End Property ''' <summary>ヴェロシティー(強さ)を取得します。</summary> Public Property Velocity() As Integer Get Return m_velocity End Get Set(ByVal value As Integer) m_velocity = value End Set End Property Public Overrides Function ToString() As String Dim result As New System.Text.StringBuilder result.Append("Delta = " & m_deltaTime & " ") result.Append("Type = " & m_dataType.ToString & " ") result.Append("channel = " & m_channel.ToString & " ") result.Append("note = " & m_note.ToString & " ") result.Append("velocity = " & m_velocity.ToString & " ") Return result.ToString End Function End Class ''' <summary> ''' SysExEvent ''' </summary> ''' <remarks></remarks> Public Class SmfSysExEvent Inherits SmfEvent Private m_dataLength As Integer Private m_originalData() As Byte ''' <summary>データ長を取得します。</summary> Public Property DataLength() As Integer Get Return m_dataLength End Get Set(ByVal value As Integer) m_dataLength = value End Set End Property ''' <summary>オリジナルのデータを取得します。</summary> Public Property OriginalData() As Byte() Get Return m_originalData End Get Set(ByVal value() As Byte) m_originalData = value End Set End Property Public Overrides Function ToString() As String Return "SmfSysExEvent" End Function End Class ''' <summary> ''' MetaEvent ''' </summary> ''' <remarks></remarks> Public Class SmfMetaEvent Inherits SmfEvent Private m_dataType As SmfReader.MetaEventType Private m_dataLength As Integer Private m_dataValue As Integer Private m_text As String = "" Private m_originalData() As Byte Public Property DataType() As SmfReader.MetaEventType Get Return m_dataType End Get Set(ByVal value As SmfReader.MetaEventType) m_dataType = value End Set End Property Public Property DataLength() As Integer Get Return m_dataLength End Get Set(ByVal value As Integer) m_dataLength = value End Set End Property Public Property DataValue() As Integer Get Return m_dataValue End Get Set(ByVal value As Integer) m_dataValue = value End Set End Property Public Property Text() As String Get Return m_text End Get Set(ByVal value As String) m_text = value End Set End Property Public Property OriginalData() As Byte() Get Return m_originalData End Get Set(ByVal value() As Byte) m_originalData = value End Set End Property Public Overrides Function ToString() As String Dim result As New System.Text.StringBuilder result.Append("Delta = " & m_deltaTime & " ") result.Append("Type = " & m_dataType.ToString & " ") result.Append("text = " & m_text.ToString & " ") Return result.ToString End Function End Class