안녕하세요. 언제나휴일입니다.
1. 미디 이벤트
앞에서 우리는 미디 파일은 청크들의 집합이라는 것과 청크에는 Header 청크와 Track 청크가 있다는 것을 확인하였습니다. 그리고 Track 청크에는 이벤트 정보들이 있고 이벤트 정보에는 메타 이벤트, 미디 이벤트, 시스템 이벤트가 있다는 것도 소개하였습니다.
바로 이전 강의에서는 메타 이벤트를 분석하는 코드를 작성했었죠.
이번에는 미디 이벤트를 분석하는 코드를 작성하기로 할게요.
Track 청크는 이벤트들로 구성한다고 앞에서 얘기를 했고 이벤트는 delta time이 오고 그 뒤에 오는 상태 정보가 0~0xEF까지는 미디 이벤트, 0xF0~0xFE까지는 시스템 이벤트, 0xFF는 메타 이벤트라고 하였습니다.
미디 이벤트는 이벤트 타입의 값에 따라 다양한 의미를 지니고 있어요.
8X Note Velocity : Note on
9X Note Velocity : Note off
*X는 채널 번호*
Note on과 Note off는 소리를 내거나 소리를 끄는 이벤트입니다.
이때 상태 Bytes의 하위 4비트는 채널 번호를 의미합니다.
그리고 다음 바위트에 건반 번호(0부터 127, 음 번호)가 오며 다음 바이트에 건반을 누르는 속도(결국 소리의 세기)가 옵니다.
만약 Note on 이벤트에서 소리의 크기가 0일 때는 Note off로 간주할 수 있습니다.
예를 들어 91 40 54라는 값이 오면 Note on 이벤트입니다.
채널은 1이며 건반은 64(0x40)+1번째 건반입니다.
그리고 세기는 0x54만큼입니다.
AX Note Velocity : Key after touch
*X는 채널 번호*
Key after touch는 건반을 누른 상태에서 다시 압력을 가하는 것을 말하며 이를 허용하는 전자 악기가 있습니다.
이때 상태 Bytes의 하위 4비트는 채널 번호를 의미합니다.
다음 두 개의 바이트는 건반 번호와 누르는 속도입니다.
BX Control Value : Control Change
*X는 채널 번호*
Control Change는 효과를 바꾸는 이벤트입니다.
다음 바이트는 컨트롤 번호이며 다음 바이트는 새로운 값입니다.
CX Program :Program Change
*X는 채널 번호*
Program Change는 두 개의 바이트로 구성합니다.
상태 바이트 다음 바이트는 프로그램 번호(악기)입니다.
DX Pressure : Channel after touch
*X는 채널 번호*
Channel after touch는 두 개의 바이트로 구성합니다.
상태 바이트 다음 바이트는 프로그램 번호(악기)입니다.
EX Buttom7 Top7 : Pitch wheel change
*X는 채널 번호*
Pitch wheel change는 세 개의 바이트로 구성합니다.
상태 바이트 다음 바이트는 least 값이며 다음 바이트는 most 값입니다.
다음은 Note값과 대응하는 음(열)과 옥타브(행)을 나타낸 표입니다.
상태 바이트의 값이 0x7F 이하일 때는 별도의 상태 값이 없고 이전 이벤트의 상태 값을 유지합니다.
이를 Running Status라고 부릅니다.
예를 들어 91 4A 54 91 50 48 91 3A 45를 Running Status를 적용하여 표현하면
91 4A 54 50 48 3A 45 처럼 표현할 수 있습니다.
이번 강의에서는 미디 이벤트를 분할하는 부분까지 구현하고 다음 강의에서 미디 이벤트를 상세 구현할게요.
2. MDEvent 클래스 추가 구현
ehmidilib 프로젝트의 MDEvent 클래스에 미디 이벤트를 생성하는 코드를 추가합시다.
Parsing 메서드에 이벤트 타입이 0xF0보다 작을 때 미디 이벤트를 생성하는 코드를 추가합니다.
if(buffer[offset]<0xF0) { return MidiEvent.MakeEvent(buffer[offset++], delta, buffer, ref offset, oldoffset, mdevent.EventType); }
다음은 현재까지 작성한 MDEvent.cs 소스 코드입니다.
namespace ehmidi { public class MDEvent { public int Delta { get; } public byte EventType { get; } public byte[] Buffer { get; } public MDEvent(byte evtype, int delta, byte[] buffer) { EventType = evtype; Delta = delta; Buffer = buffer; } public static MDEvent Parsing(byte[] buffer, ref int offset, MDEvent mdevent) { int oldoffset = offset; int delta = StaticFuns.ReadDeltaTime(buffer, ref offset); if(buffer[offset] == 0xFF) { offset++; return MetaEvent.MakeEvent(delta, buffer, ref offset, oldoffset); } if(buffer[offset]<0xF0) { return MidiEvent.MakeEvent(buffer[offset++], delta, buffer, ref offset, oldoffset, mdevent.EventType); } return null;//차후에 구현 } } }
3. MidiEvent 클래스 정의
MidiEvent는 MDEvent를 기반으로 파생한 클래스로 정의합니다.
public class MidiEvent:MDEvent {
멤버 속성으로 미디 이벤트의 첫 번째 바이트, 두 번째 바이트 및 상태 정보를 제공합시다.
public byte Fdata { get; } public byte Sdata { get; } public string Status { get { if(EventType<0x80) { return "Running Status"; } switch(EventType>>4) { case 0x8: return "Note Off"; case 0x9: return "Note On"; case 0xA: return "Note after touch"; case 0xB: return "Controller"; case 0xC: return "Changer Instrument"; case 0xD: return "Channel after touch"; case 0xE: return "Pitch Bend"; } return string.Empty; } }
생성자에서는 기반 형식 이니셜라이즈를 해 주고 Fdata와 SData 속성을 설정합니다.
public MidiEvent(byte etype, int delta, byte fdata, byte sdata, byte[] buffer):base(etype, delta,buffer) { Fdata = fdata; Sdata = sdata; }
MakeEvent 정적 메서드를 구현합시다.
public static MDEvent MakeEvent(byte etype, int delta, byte[] buffer, ref int offset, int oldoffset, byte be_evtype) {
etype(이벤트 타입)이 0x80보다 작을 때 Running Status입니다.
이 때는 etype 값이 첫 번째 바이트로 사용하며 이벤트 타입은 이전 이벤트 타입인 be_evtype입니다.
0x80보다 작지 않을 때는 buffer[offset]값이 첫 번째 바이트입니다.
byte fdata; byte sdata=0; if(etype<0x80) { fdata = etype; etype = be_evtype; } else { fdata = buffer[offset++]; }
0x80~0xEF까지는 두 번째 바이트 값도 존재합니다. etype을 우측으로 4비트 이동한 값에 따라 두 번째 바이트 값을 처리하는 코드를 작성할게요.
switch (etype >> 4) { case 0x8://Note Off case 0x9: //Note On case 0xA: //Note after touch case 0xB: //Controller case 0xE: //Pitch Bend sdata = buffer[offset++]; break; case 0xC: //Change Instrument case 0xD: //Channel after touch1 break; default: return null; }
현재 분석한 이벤트 정보를 버퍼로 복사한 후에 미디 이벤트를 생성하여 반환합니다.
byte[] buffer2 = new byte[offset - oldoffset]; Array.Copy(buffer, oldoffset, buffer2, 0, buffer2.Length); return new MidiEvent(etype, delta, fdata, sdata, buffer2);
다음은 현재까지 작성한 MidiEvent.cs 소스 코드입니다.
using System; namespace ehmidi { public class MidiEvent:MDEvent { public byte Fdata { get; } public byte Sdata { get; } public string Status { get { if(EventType<0x80) { return "Running Status"; } switch(EventType>>4) { case 0x8: return "Note Off"; case 0x9: return "Note On"; case 0xA: return "Note after touch"; case 0xB: return "Controller"; case 0xC: return "Changer Instrument"; case 0xD: return "Channel after touch"; case 0xE: return "Pitch Bend"; } return string.Empty; } } public MidiEvent(byte etype, int delta, byte fdata, byte sdata, byte[] buffer):base(etype, delta,buffer) { Fdata = fdata; Sdata = sdata; } public static MDEvent MakeEvent(byte etype, int delta, byte[] buffer, ref int offset, int oldoffset, byte be_evtype) { byte fdata; byte sdata=0; if(etype<0x80) { fdata = etype; etype = be_evtype; } else { fdata = buffer[offset++]; } switch (etype >> 4) { case 0x8://Note Off case 0x9: //Note On case 0xA: //Note after touch case 0xB: //Controller case 0xE: //Pitch Bend sdata = buffer[offset++]; break; case 0xC: //Change Instrument case 0xD: //Channel after touch1 break; default: return null; } byte[] buffer2 = new byte[offset - oldoffset]; Array.Copy(buffer, oldoffset, buffer2, 0, buffer2.Length); return new MidiEvent(etype, delta, fdata, sdata, buffer2); } } }
4. 테스트
솔루션에 콘솔 앱(.NET Framework) 프로젝트를 추가하세요.
프로젝트 이름은 “미디 이벤트 분석”이라고 할게요.
먼저 ehmidilib를 참조 추가합니다.
미디 파일도 기존 항목에 추가하고 파일 속성에서 출력 폴더에 항상 복사를 설정합니다.
테스트 코드는 지난 번 메타 이벤트 분석 코드에 미디 이벤트 정보를 출력하는 부분을 추가한 코드입니다.
using ehmidi; using System; using System.IO; namespace 미디_이벤트_분석 { class Program { static string fname = "moz_sleep.mid"; static void Main(string[] args) { FileStream fs = new FileStream(fname, FileMode.Open); while (fs.Position < fs.Length) { Chunk chunk = Chunk.Parse(fs); if (chunk != null) { Console.WriteLine("{0}:{1}bytes", chunk.CTString, chunk.Length); } if (chunk is Header) { ViewHeader(chunk as Header); } if (chunk is Track) { ViewTrack(chunk as Track); } } } private static void ViewTrack(Track track) { Console.WriteLine("==== Track Chunk ===="); int ecnt = 0; foreach (MDEvent mdevent in track) { ecnt++; Console.WriteLine(StaticFuns.HexaString(mdevent.Buffer)); Console.WriteLine("{0}th delta:{1}", ecnt, mdevent.Delta); if (mdevent is MetaEvent) { Console.Write("<Meta> "); ViewMeta(mdevent as MetaEvent); } if(mdevent is MidiEvent) { Console.Write("<Midi> "); ViewMidi(mdevent as MidiEvent); } } } private static void ViewMidi(MidiEvent midievent) { Console.WriteLine(midievent.Status); } private static void ViewMeta(MetaEvent me) { Console.WriteLine("메시지:{0} 길이:{1} ", me.Msg, me.Length); Console.WriteLine(me.MetaDescription); } private static void ViewHeader(Header header) { Console.WriteLine("==== 헤더 Chunk ===="); Console.WriteLine(StaticFuns.HexaString(header.Buffer)); Console.WriteLine("Format:{0}", header.Format); Console.WriteLine("Tracks:{0}", header.TrackCount); Console.WriteLine("Division:{0}", header.Division); Console.WriteLine(); } } }