先日、ExcelでMIDIファイルを作るマクロを作ったので、調子に乗って、ExcelでMIDIシーケンサーを作ってみました。 「ドレミの歌」くらいなら、すぐにMIDIファイルを作って遊べます。
長いですが、ソースコードを以下に載せます。VBAのモジュールにコピペすれば使えるはずです。
Excel VBAで直接MIDIを演奏するには、APIが必要なので面倒です。ですが、MIDIファイルを作るだけなら、バイナリファイルをいじるだけなので簡単です。というわけで演奏機能はありません。
MIDIファイルの詳細については
Wikipedia等を参照ください。
興味ある方は、ご自由にご利用ください。
2016/2/6
SMF1に対応させました。ユーザーインターフェースをマルチトラックに対応させるのが面倒だったので、1トラックだけ対応です。
--------------------------------------------------
Option Explicit
'==================================================
'MIDIファイルの構造 (SMF, Standard Midi File)
'<HeaderChunk> + <TrackChunk> + <TrackChunk> ...
'
'MIDIファイルには複数のフォーマットがある (SMF0, SMF1, SMF2)
'SMF0 : TrackChunkは1つ
'SMF1 : TrackChunkは複数 (1つのTrackには1つのChannelが対応, 最初のTrackにはテンポなどの情報だけ入れる(コンダクタトラック))
'SMF2 : 使われていないらしいので省略
'==================================================
'<HeaderChunk>
'ChunkID 4bytes, "MThd"
'ChunkSize 4bytes, Big Endian, 6
'FormatType 2bytes, Big Endian, 0 or 1
'NumberOfTracks 2bytes, Big Endian, SMF0なら1
'TimeDivision 2bytes, Big Endian
'
'TimeDivisionについて
'(1) bit15が0の場合(0*** **** **** ****)
'4分音符の分割数 (48, 96, 960, etc)
'4分音符の実際の時間は、MidiEventのMetaEventで設定可能 (デフォルトは0.5sec)
'
'(2) bit15が1の場合(1aaa aaaa bbbb bbbb)
'1aaa aaaaが1秒間のフレーム数 (-24, -25, -29, -30)
'bbbb bbbbが1フレームの分解能 (4, 8, 10, 80, 100, etc)
'
'(1)を使うことが多い
'なお、MIDIでは最小時間単位をtickという
'==================================================
'<TrackChunk>
'ChunkID 4bytes, "MTrk"
'ChunkSize 4bytes, Big Endian
'Data MidiEventの配列
'
'MidiEventには、ChannelEvent , SystemEvent, MetaEventの3種類ある(最初の1byteで区別する)
'
'variable(可変長)について
'最上位bitが1なら、次の1byteもデータと見なす
'(1xxx xxxx 1yyy yyyy 0zzz zzzz) は (xxx xxxx yyy yyyy zzz zzzz)を意味する
'最大4bytes (データは最大で28bits)
'
'==================================================
'<ChannelEvent>
'DeltaTime variable, Big Endian, 直前のMidiEventからの時間 (tick単位)
'EventType 1byte, 0x80-0xEF
'Param1 1byte
'Param2 1byte, EventTypeによっては存在しない
'
'0x 8n kk vv 3bytes, Note Off
'0x 9n kk vv 3bytes, Note On
'0x An kk vv 3bytes, Note Aftertouch
'0x Bn cc dd 3bytes, Controller
'0x Cn pp 2bytes, Program Change
'0x Dn vv 2bytes, Channel Aftertouch
'0x En ll mm 3bytes, Pitch Bend
'
'n 4bits, 0-15, Channel Number
'kk 1byte, 0-127, Note Number
'vv 1byte, 0-127, Velocity
'cc 1byte, 0-127, Controller Type
'dd 1byte, 0-127, Controller Value
'pp 1byte, 0-127, Program Number
'll 1byte, 0-127, Pitch Value
'mm 1byte, 0-127, Pitch Value
'
'ChannelEventでは、同じChannelEventが連続するときは、2回目以降が省略可能 (Running Status)
'1つのChannelには1つの楽器を対応させる (n=9はパーカッションに固定されている) (SMF1なら1つのTrackが1つのChannel、1つの楽器に対応する)
'ControllerについてはWikipedia etcを参照
'Program NumberについてはWikipedia etcを参照
'llとmmは2つで1つのパラメータ(0-16383、8192のときはPitchの変更なし)
'll=0xxxxxxx, mm=0yyyyyyyは、yyyyyyyxxxxxxxを意味する(Little Endian)
'==================================================
'<SystemEvent>
'DeltaTime variable, Big Endian, 直前のMidiEventからの時間 (tick単位)
'EventType 1byte, 0xF0 or 0xF7
'DataLength variable, Dataのサイズ(byte単位)
'Data
'
'Dataが長い場合は、複数のSystem Eventにすることもある(送信エラーも起こりうる)
'Dataの最初なら、System EventはF0で始める
'分割されたDataの途中なら、System EventはF7で始める(variableは分割されたDataのサイズ)
'Dataの最後にはF7を付ける (variableにはF7の1byteも含める)
'==================================================
'<Meta Event>
'DeltaTime variable, Big Endian, 直前のMidiEventからの時間 (tick単位)
'EventType 1byte, 0xFF
'MetaEventType 1byte, 種類はたくさんある
'DataLength variable, Dataのサイズ(byte単位)
'Data
'
'0x FF 00 シーケンス番号 (Dataは2bytes)
'0x FF 01 テキスト
'0x FF 02 著作権
'0x FF 03 シーケンス名
'0x FF 04 楽器名
'0x FF 05 歌詞
'0x FF 06 マーカー
'0x FF 07 キューポイント
'0x FF 20 MIDIチャンネルプリフィックス (Dataは1byte)
'0x FF 21 ポート指定 (Dataは1byte。非標準。)
'0x FF 2F トラック終端 (必須。Dataは0byte)
'0x FF 51 テンポ (Dataは3bytes) 4分音符の秒数(usec単位) デフォルトは500000usec
'0x FF 54 オフセット (Dataは3bytes) hr mn se fr ff 0-23, 0-59, 0-59, 0-30, 0-99の値をとるらしい
'0x FF 58 拍子 (Dataは4bytes) 4/4拍子とか
'0x FF 59 調号 (Dataは2bytes) シャープ、フラット、長調、短調
'0x FF 7F シーケンサー特定メタイベント
'==================================================
'参考URL
'http://ja.wikipedia.org/wiki/General_MIDI
'http://www.midi.org/techspecs/midimessages.php
'==================================================
Private Type HEADER_CHUNK
ChunkID As String * 4
ChunkSize As Long
FormatType As Integer
NumberOfTracks As Integer
TimeDivision As Integer
End Type
Private Type TRACK_CHUNK
ChunkID As String * 4
ChunkSize As Long
Data() As Byte
End Type
Private Type MIDI_EVENT
DeltaTime As Long
EventType As Byte
Param1 As Byte
Param2 As Byte
End Type
Public Sub seq_sample()
Dim i As Long
Dim note() As String
Dim key1 As Byte
Dim key2 As Byte
Dim time_step As Long
Dim time_count As Long
ReDim note(11) As String
note(0) = "C": note(1) = "C#": note(2) = "D": note(3) = "D#"
note(4) = "E": note(5) = "F": note(6) = "F#": note(7) = "G"
note(8) = "G#": note(9) = "A": note(10) = "A#": note(11) = "B"
Sheet1.Cells.ClearContents
'------------------------------
Sheet1.Cells(1, 1) = "Program No. : "
Sheet1.Cells(1, 2) = 0
'------------------------------
key1 = 48
key2 = 83
Sheet1.Cells(2, 1) = "Key"
For i = 0 To key2 - key1
Sheet1.Cells(3 + i, 1) = key2 - i
Sheet1.Cells(3 + i, 2) = note((key2 - i) Mod 12)
Next i
'------------------------------
time_step = 24
time_count = 480 '四分音符1個 = 0.5sec = 96tickのときの60secに相当
Sheet1.Cells(1, 3) = "Key"
For i = 0 To time_count - 1
Sheet1.Cells(2, 3 + i) = i * time_step
Next i
Sheet1.Range(Sheet1.Cells(1, 1), Sheet1.Cells(1, 2 + time_count)).ColumnWidth = 4
'------------------------------
Sheet1.Cells(26, 3) = "on"
Sheet1.Cells(26, 4) = "off"
Sheet1.Cells(24, 4) = "on"
Sheet1.Cells(24, 5) = "off"
Sheet1.Cells(22, 5) = "on"
Sheet1.Cells(22, 6) = "off"
Sheet1.Cells(21, 6) = "on"
Sheet1.Cells(21, 7) = "off"
Sheet1.Cells(19, 7) = "on"
Sheet1.Cells(19, 8) = "off"
Sheet1.Cells(17, 8) = "on"
Sheet1.Cells(17, 9) = "off"
Sheet1.Cells(15, 9) = "on"
Sheet1.Cells(15, 10) = "off"
Sheet1.Cells(14, 10) = "on"
Sheet1.Cells(14, 14) = "off"
'------------------------------
ActiveWindow.FreezePanes = False
Sheet1.Cells(3, 3).Select
ActiveWindow.FreezePanes = True
End Sub
Public Sub seq_write()
Dim i As Long
Dim j As Long
Dim row As Long
Dim col As Long
Dim count As Long
Dim str As String
Dim prg As Byte
Dim ch As Byte
Dim time As Long
Dim time_last As Long
Dim key As Byte
Dim filename As String
Dim hChunk As HEADER_CHUNK
Dim tChunk() As TRACK_CHUNK
Dim midi() As MIDI_EVENT
'------------------------------
hChunk.ChunkID = "MThd"
hChunk.ChunkSize = 6
hChunk.FormatType = 1
hChunk.NumberOfTracks = 2
hChunk.TimeDivision = 96
'------------------------------
ReDim tChunk(1) As TRACK_CHUNK
'------------------------------
ReDim midi(0) As MIDI_EVENT
setMidi midi(0), 0, &HFF, &H2F, &H0
encodeMidi tChunk(0), midi, 1
Erase midi
'------------------------------
'MidiEventイベントの数を調べる
row = Sheet1.Cells(3, 1).End(xlDown).row - 2
col = Sheet1.Cells(2, 3).End(xlToRight).column - 2
count = 0
For i = 0 To col - 1
For j = 0 To row - 1
str = Sheet1.Cells(3 + j, 3 + i)
If str = "on" Then
count = count + 1
ElseIf str = "off" Then
count = count + 1
Else
End If
Next j
Next i
'------------------------------
ReDim midi(count + 1) As MIDI_EVENT
prg = CByte(Sheet1.Cells(1, 2))
ch = CByte(1)
count = 0
time = 0
time_last = 0
setMidi midi(count), 0, &HC0, prg, &H0
count = count + 1
For i = 0 To col - 1
For j = 0 To row - 1
str = Sheet1.Cells(3 + j, 3 + i)
time = Sheet1.Cells(2, 3 + i)
key = CByte(Sheet1.Cells(3 + j, 1))
If str = "on" Then
setMidi midi(count), time - time_last, CByte(&H90 + ch), key, &H7F
count = count + 1
time_last = time
ElseIf str = "off" Then
setMidi midi(count), time - time_last, CByte(&H80 + ch), key, &H7F
count = count + 1
time_last = time
Else
End If
Next j
Next i
setMidi midi(count), 0, &HFF, &H2F, &H0
count = count + 1
encodeMidi tChunk(1), midi, count
Erase midi
'------------------------------
ChDir ThisWorkbook.Path
filename = "sequence.mid"
writeMidi filename, hChunk, tChunk
End Sub
Private Sub setMidi(ByRef midi As MIDI_EVENT, dt As Long, et As Byte, p1 As Byte, p2 As Byte)
midi.DeltaTime = dt
midi.EventType = et
midi.Param1 = p1
midi.Param2 = p2
End Sub
Private Sub encodeMidi(ByRef tChunk As TRACK_CHUNK, ByRef midi() As MIDI_EVENT, ByRef size As Long)
Dim i As Long
Dim j As Long
Dim temp1 As Byte
Dim temp4 As Long
Dim buf() As Byte
ReDim buf(size * 8 - 1) As Byte
i = 0
For j = 0 To size - 1
'------------------------------
'DeltaTime
temp4 = 128
Do While temp4 <= midi(j).DeltaTime
temp4 = temp4 * 128
Loop
Do While 128 < temp4
temp4 = temp4 \ 128
buf(i) = CByte((midi(j).DeltaTime \ temp4) Mod 128 + &H80)
i = i + 1
Loop
buf(i) = CByte(midi(j).DeltaTime Mod 128)
i = i + 1
'------------------------------
'EventType
If 0 < j And midi(j).EventType < &HF0 And midi(j).EventType = temp1 Then
'running status
Else
temp1 = midi(j).EventType
buf(i) = temp1
i = i + 1
End If
'------------------------------
'Data
If temp1 < &HF0 Then
If &HC0 <= temp1 And temp1 < &HE0 Then
buf(i) = midi(j).Param1
i = i + 1
Else
buf(i) = midi(j).Param1
buf(i + 1) = midi(j).Param2
i = i + 2
End If
Else
'System Event, Meta Eventには未対応
End If
'------------------------------
'Track終端メッセージ
If midi(j).EventType = &HFF And midi(j).Param1 = &H2F Then
buf(i) = &H2F
buf(i + 1) = &H0
i = i + 2
Exit For
End If
'------------------------------
Next j
tChunk.ChunkID = "MTrk"
tChunk.ChunkSize = i
ReDim tChunk.Data(tChunk.ChunkSize - 1) As Byte
For j = 0 To i - 1
tChunk.Data(j) = buf(j)
Next j
Erase buf
End Sub
Private Sub decodeMidi(ByRef tChunk As TRACK_CHUNK, ByRef midi() As MIDI_EVENT, ByRef size As Long)
Dim i As Long
Dim j As Long
Dim temp1 As Byte
Dim temp4 As Long
Dim buf() As MIDI_EVENT
ReDim buf(tChunk.ChunkSize / 2 - 1) As MIDI_EVENT
i = 0
j = 0
Do While i < tChunk.ChunkSize
'------------------------------
'DeltaTime
temp4 = tChunk.Data(i) And &H7F
i = i + 1
Do While (tChunk.Data(i - 1) And &H80) = &H80
temp4 = temp4 * 128 + (tChunk.Data(i) And &H7F)
i = i + 1
Loop
buf(j).DeltaTime = temp4
'------------------------------
'EventType
If (tChunk.Data(i) And &H80) = &H80 Then
temp1 = tChunk.Data(i)
i = i + 1
End If
buf(j).EventType = temp1
'------------------------------
'Data
If temp1 < &HF0 Then
If &HC0 <= temp1 And temp1 < &HE0 Then
buf(j).Param1 = tChunk.Data(i)
buf(j).Param2 = 0
i = i + 1
Else
buf(j).Param1 = tChunk.Data(i)
buf(j).Param2 = tChunk.Data(i + 1)
i = i + 2
End If
Else
'System Event, Meta Eventには未対応
If temp1 = &HFF Then
buf(j).Param1 = tChunk.Data(i)
i = i + 1
End If
temp4 = tChunk.Data(i) And &H7F
i = i + 1
Do While (tChunk.Data(i - 1) And &H80) = &H80
temp4 = temp4 * 128 + (tChunk.Data(i) And &H7F)
i = i + 1
Loop
i = i + temp4
End If
'------------------------------
j = j + 1
Loop
size = j
ReDim midi(size - 1) As MIDI_EVENT
For i = 0 To j - 1
midi(i).DeltaTime = buf(i).DeltaTime
midi(i).EventType = buf(i).EventType
midi(i).Param1 = buf(i).Param1
midi(i).Param2 = buf(i).Param2
Next i
Erase buf
End Sub
'ファイルに書き込む
Private Sub writeMidi(filename As String, ByRef hChunk As HEADER_CHUNK, ByRef tChunk() As TRACK_CHUNK)
Dim i As Long
Dim buf2(1) As Byte
Dim buf4(3) As Byte
Open filename For Binary As 1
'------------------------------
Put 1, , hChunk.ChunkID
buf4(3) = CByte(hChunk.ChunkSize Mod 256)
buf4(2) = CByte((hChunk.ChunkSize \ 256) Mod 256)
buf4(1) = CByte(((hChunk.ChunkSize \ 256) \ 256) Mod 256)
buf4(0) = CByte((((hChunk.ChunkSize \ 256) \ 256) \ 256) Mod 256)
Put 1, , buf4
buf2(1) = CByte(hChunk.FormatType)
buf2(0) = CByte(0)
Put 1, , buf2
buf2(1) = CByte(hChunk.NumberOfTracks Mod 256)
buf2(0) = CByte((hChunk.NumberOfTracks \ 256) Mod 256)
Put 1, , buf2
buf2(1) = CByte(hChunk.TimeDivision Mod 256)
buf2(0) = CByte((hChunk.TimeDivision \ 256) Mod 256)
Put 1, , buf2
'------------------------------
For i = 0 To hChunk.NumberOfTracks - 1
Put 1, , tChunk(i).ChunkID
buf4(3) = CByte(tChunk(i).ChunkSize Mod 256)
buf4(2) = CByte((tChunk(i).ChunkSize \ 256) Mod 256)
buf4(1) = CByte(((tChunk(i).ChunkSize \ 256) \ 256) Mod 256)
buf4(0) = CByte((((tChunk(i).ChunkSize \ 256) \ 256) \ 256) Mod 256)
Put 1, , buf4
Put 1, , tChunk(i).Data
Next i
'------------------------------
Close 1
End Sub
'ファイルから読み込む
Private Sub readMidi(filename As String, ByRef hChunk As HEADER_CHUNK, ByRef tChunk() As TRACK_CHUNK)
Dim i As Long
Dim buf2(1) As Byte
Dim buf4(3) As Byte
On Error GoTo Label1
Open filename For Binary As 1
'------------------------------
Get 1, , buf4
hChunk.ChunkID = Chr(buf4(0)) & Chr(buf4(1)) & Chr(buf4(2)) & Chr(buf4(3))
Get 1, , buf4
hChunk.ChunkSize = ((CLng(buf4(0)) * 256 + buf4(1)) * 256 + buf4(2)) * 256 + buf4(3)
Get 1, , buf2
hChunk.FormatType = CInt(buf2(0)) * 256 + buf2(1)
Get 1, , buf2
hChunk.NumberOfTracks = CInt(buf2(0)) * 256 + buf2(1)
Get 1, , buf2
hChunk.TimeDivision = CInt(buf2(0)) * 256 + buf2(1)
ReDim tChunk(hChunk.NumberOfTracks - 1) As TRACK_CHUNK
'------------------------------
For i = 0 To hChunk.NumberOfTracks - 1
Get 1, , buf4
tChunk(i).ChunkID = Chr(buf4(0)) & Chr(buf4(1)) & Chr(buf4(2)) & Chr(buf4(3))
Get 1, , buf4
tChunk(i).ChunkSize = ((CLng(buf4(0)) * 256 + buf4(1)) * 256 + buf4(2)) * 256 + buf4(3)
ReDim tChunk(i).Data(tChunk(i).ChunkSize - 1) As Byte
Get 1, , tChunk(i).Data
Next i
'------------------------------
Close 1
Exit Sub
Label1:
Close 1
MsgBox "error", vbExclamation
End Sub