프로그래밍 기술/미디 파일 구조 및 미디 분석 프로그램 만들기

[미디 파일] 미디 파일 구조 분석 및 프로그래밍 6 - Track 청크 4, 미디 이벤트 분할

언제나휴일 2018. 4. 16. 15:23
반응형

[미디 파일] 미디 파일 구조 분석 및 프로그래밍 6 - Track 청크 4, 미디 이벤트 분할



 안녕하세요. 언제나 휴일, 언휴예요.


 앞에서 우리는 미디 파일은 청크들의 집합이라는 것과 청크에는 Header 청크와 Track 청크가 있다는 것을 확인하였습니다. 그리고 Track 청크에는 이벤트 정보들이 있고 이벤트 정보에는 메타 이벤트, 미디 이벤트, 시스템 이벤트가 있다는 것도 소개하였습니다. 바로 이전 글에서는 메타 이벤트에서의 정보에 관한 사항을 언급했었죠.

[미디 파일] 미디 파일 구조 분석 및 프로그래밍 1 - 청크 목록

[미디 파일] 미디 파일 구조 분석 및 프로그래밍 2 - Head 청크, Mthd

[미디 파일] 미디 파일 구조 분석 및 프로그래밍 3 - Track 청크 1, delta time 구하기

[미디 파일] 미디 파일 구조 분석 및 프로그래밍 4 - Track 청크 2, Meta Event

[미디 파일] 미디 파일 구조 분석 및 프로그래밍 5 - Track 청크 3, 박자, 키 정보 등


 이번에는 트랙 청크의 이벤트 중에서 미디 이벤트 들이 있는 부분을 미디 이벤트 단위로 분할하는 작업을 진행해 보기로 할게요.


 Track 청크는 이벤트들로 구성한다고 앞에서 얘기를 했고 이벤트는 delta time이 오고 그 뒤에 오는 상태 정보가 0~0xEF까지는 미디 이벤트, 0xF0~0xFE까지는 시스템 이벤트, 0xFF는 메타 이벤트라고 하였습니다.


 여기에서는 상태 정보가 0~0xEF 사이에 오는 미디 이벤트에 관해서 간략하게 다룰거예요.


 상태 정보가 0~0x7F사이의 값이 오는 것은 이전의 이벤트와 같은 상태 값을 갖는 Running Status에 해당합니다. 이 부분은 다른 항목을 설명한 후에 설명하기로 할게요.


 미디 이벤트에 관한 정보는 크게 다음처럼 구분할 수가 있어요.


8X Note off (소리 끄기)
9X Note on (소리 내기)
AX Key after touch (건반을 누른 상태에서 다시 압력을 가함)
BX Control Change (효과 바꾸기)
CX Program change (악기 바꾸기)
DX Channel after touch
EX Pitch wheel change

참고로 미디 이벤트에서 상태 Bytes의 하위 4비트는 채널 번호를 의미합니다.


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 값입니다.


다음은 건반 번호에 해당하는 음을 나타낸 표입니다. 행은 옥타브를 의미합니다.

 

C

C#

D

D#

E

F

F#

G

G#

A

A#

B

0

0

1

2

3

4

5

6

7

8

9

10

11

1

12

13

14

15

16

17

18

19

20

21

22

23

2

24

25

26

27

28

29

30

31

32

33

34

35

3

36

37

38

39

40

41

42

43

44

45

46

47

4

48

49

50

51

52

53

54

55

56

57

58

59

5

60

61

62

63

64

65

66

67

68

69

70

71

6

72

73

74

75

76

77

78

79

80

81

82

83

7

84

85

86

87

88

89

90

91

92

93

94

95

8

96

97

98

99

100

101

102

103

104

105

106

107

9

108

109

110

111

112

113

114

115

116

117

118

119

10

120

121

122

123

124

125

126

127

 

 

 

 


상태 바이트의 값이 0x7F 이하일 때는 별도의 상태 값이 없고 이전 이벤트의 상태 값을 유지합니다.

이를 Running Status라고 부릅니다. 


예를 들어 91 4A 54 91 50 48 91 3A 45를 Running Status를 적용하여 표현하면

91 4A 54 50 48 3A 45 처럼 표현할 수 있습니다.


다음은 앞에서 작성했던 미디 파일 분석하는 프로그램에 미디 이벤트 부분을 분할하는 코드를 추가 작성한 코드입니다.


[StaticFuns.cs] 공통으로 사용할 함수를 래핑한 정적 클래스

 using System;

using System.Net;

using System.Text;

 

namespace 미디_이벤트_분석

{

    public static class StaticFuns

    {

        public static string GetString(int magic)

        {

            byte[] data = BitConverter.GetBytes(magic);

            ASCIIEncoding en = new ASCIIEncoding();

            return en.GetString(data);

        }

 

        public static short ConvertHostorderS(byte[] data, int offset)

        {

            return ConvertHostorder(BitConverter.ToInt16(data, offset));

        }

        public static short ConvertHostorder(short data)

        {

            return IPAddress.NetworkToHostOrder(data);

        }

 

        public static string GetString(byte[] data)

        {

            char[] buf = new char[data.Length];

            for (int i = 0; i < data.Length; i++)

            {

                buf[i] = (char)data[i];

            }

            Encoding en = Encoding.Default;

            return en.GetString(data);

        }

 

        public static int ConvertHostorder(int data)

        {

            return IPAddress.NetworkToHostOrder(data);

        }

 

        public static string HexaString(byte[] buffer)

        {

            string str = "";

            foreach (byte d in buffer)

            {

                str += string.Format("{0:X2} ", d);

            }

            return str;

        }

        public static int ReadDeltaTime(byte[] buffer, ref int offset)

        {

            int time = 0;

            byte b;

            do

            {

                b = buffer[offset];

                offset++;

                time = (time<<7)|(b & 0x7F);

            } while (b > 127);

            return time;

        }

    }

}

 


[MDEvent.cs]

namespace 미디_이벤트_분석

{

    public class MDEvent

    {

        public int Delta

        {

            get;

            private set;

        }

        public byte EventType

        {

            get;

            private set;

        }

        public byte[] Buffer

        {

            get;

            private set;

        }

 

        public MDEvent(byte evtype,int delta,byte[] buffer)

        {

            EventType = evtype;

            Delta = delta;

            Buffer = buffer;

        }

 

        public static MDEvent Parsing(byte[] buffer, ref int offset,MDEvent bef_event)

        {

            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,bef_event.EventType);

            }

 

 

            return null; //시스템 이벤트 부분은 차후에 구현 

        }

    }

}

 


[MetaEvent.cs]

using System;

 

namespace 미디_이벤트_분석

{

    public class MetaEvent:MDEvent

    {

        public byte Msg //어떤 종류의 메타 이벤트인지를 판별

        {

            get;

            private set;

        }

        public byte Length //메타 데이터 길이

        {

            get;

            private set;

        }

        public byte[] Data//메타 데이터

        {

            get;

            private set;

        }

        public string DataString//메타 데이터를 문자열로 변환한 값

        {

            get

            {

                if(Data == null)

                {

                    return string.Empty;

                }

                return StaticFuns.GetString(Data);

            }

        }

        public string MetaDescription

        {

            get

            {

                switch(Msg)

                {

                    case 0x00: return string.Format("SeqNo:{0}" + BitConverter.ToInt16(Data, 0));

                    case 0x01: return DataString;

                    case 0x02: return "Copyright:" + DataString;

                    case 0x03: return "Track Name:" + DataString;

                    case 0x04: return "Instument:" + DataString;

                    case 0x05: return "Lyric:"+DataString;

                    case 0x06: return "Marker:"+DataString;

                    case 0x07: return "CuePoint:"+DataString;

                    case 0x08: return "ProgramName"+DataString;

                    case 0x09: return "DeviceName"+DataString;

                    case 0x20: return "Channel:"+Data[0].ToString();

                    case 0x21: return "Midi Port:"+Data[0].ToString();

                    case 0x2F: return "End of Track";

                    case 0x51: return "Tempo:" + MakeTempo();

                    case 0x54: return "SmpteOffSet";

                    case 0x58: return "TimeSignature:" + MakeTimeSig(); ;

                    case 0x59: return "KeySignature"+MakeKeySignature();

                    case 0x7F: return "SeqEvent";

                    default: return "ETC";

                }

            }

        }

 

        private string MakeTempo()

        {

            int tempo = Data[0] << 24 | Data[1] << 16 | Data[0];

            return tempo.ToString() + "microseconds/quarter note";

        }

 

        static string[] keystr = new string[]

        {

            "C Flat","G Flat","D Flat","A Flat","E Flat","B Flat","F Flat",

            "C",

            "G","D","A","E","B","F Sharp","C Sharp"

        };

        static string[] keystr2 = new string[]

        {

            "A Flat","E Flat","B Flat","F","C","G","D",

            "A",

            "E","B","F Sharp","C Sharp","G Sharp", "D Sharp", "A Sharp"

        };

        private string MakeKeySignature()

        {           

            byte ki = (byte)(Data[0] + 7);

            if(Data[1]==0)

            {

                return keystr[ki] + " Major";

            }

            return keystr2[ki] + " minor";

        }

 

        private string MakeTimeSig()

        {

            return string.Format("{0}/{1}",Data[0],Math.Pow(2, Data[1]));

        }

 

        public MetaEvent(int delta,byte msg, byte len, byte[] data,byte[] orgbuffer):base(0xFF,delta,orgbuffer)

        {

            Msg = msg;

            Length = len;

            Data = data;

        }

 

        public static MDEvent MakeEvent(int delta, byte[] buffer, ref int offset, int oldoffset)

        {

            byte msg = buffer[offset++];

            byte len = buffer[offset++];

            byte[] data = null;

            if (msg != 0x2F)

            {

                data = new byte[len];

                Array.Copy(buffer, offset, data, 0, len);

                offset += len;

            }

            byte[] buffer2 = new byte[offset - oldoffset];

            Array.Copy(buffer, oldoffset, buffer2, 0, buffer2.Length);

            return new MetaEvent(delta, msg, len, data, buffer2);

        }

    }

}

 


[MidiEvent.cs]

using System;

 

namespace 미디_이벤트_분석

{

    public class MidiEvent:MDEvent

    {  

        public byte Fdata

        {

            get;

            private set;

        }

        public byte Sdata

        {

            get;

            private set;

        }

        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);

        }

    }

}


[Chunk.cs]

using System;

using System.IO;

 

namespace 미디_이벤트_분석

{

    public class Chunk

    {

        public int CT//청크 유형

        {

            get;

            private set;

        }

        public int Length//청크 길이

        {

            get;

            private set;

        }

        public byte[] Data//데이터

        {

            get;

            private set;

        }

        public string CTString//청크 유형(문자열)

        {

            get

            {

                return StaticFuns.GetString(CT);

            }

        }

        public byte[] Buffer

        {

            get

            {

                byte[] ct_buf = BitConverter.GetBytes(CT);

                int belen = StaticFuns.ConvertHostorder(Length);

                byte[] len_buf = BitConverter.GetBytes(belen);

                byte[] buffer = new byte[ct_buf.Length + len_buf.Length + Data.Length];

                Array.Copy(ct_buf, buffer, ct_buf.Length);

                Array.Copy(len_buf, 0, buffer, ct_buf.Length, len_buf.Length);

                Array.Copy(Data, 0, buffer, ct_buf.Length + len_buf.Length, Data.Length);

                return buffer;

            }

        }

        public static Chunk Parse(Stream stream)

        {

            try

            {

                BinaryReader br = new BinaryReader(stream);

                int ctype = br.ReadInt32();

                int length = br.ReadInt32();

                length = StaticFuns.ConvertHostorder(length);

                byte[] buffer = br.ReadBytes(length);

                int cval = StaticFuns.ConvertHostorder(ctype);

                switch (cval)

                {

                    case 0x4d546864: return new Header(ctype, length, buffer);

                    case 0x4d54726b: return new Track(ctype, length, buffer);

                }

                return new Chunk(ctype, length, buffer);

            }

            catch

            {

                return null;

            }

        }

        public Chunk(int ctype, int length, byte[] buffer)

        {

            CT = ctype;

            Length = length;

            Data = buffer;

        }

    }

}


[Header.cs]

namespace 미디_이벤트_분석

{

    public class Header:Chunk

    {

        public int Format//포멧

        {

            get

            {

                return StaticFuns.ConvertHostorderS(Data, 0);

            }

        }

 

        public int TrackCount//트랙 개수

        {

            get

            {

                return StaticFuns.ConvertHostorderS(Data, 2);

            }

        }

 

        public int Division//Division

        {

            get

            {

                /*첫 번째 비트가 1일 때는 다른 코드가 필요함*/

                return StaticFuns.ConvertHostorderS(Data, 4);

            }

        }

        public Header(int ctype, int length, byte[] buffer):base(ctype,length,buffer)

        {

        }

    }

}

 


[Track.cs]

using System.Collections;

using System.Collections.Generic;

 

namespace 미디_이벤트_분석

{

    public class Track:Chunk,IEnumerable

    {

        List<MDEvent> events = new List<MDEvent>();

        public Track(int ctype, int length, byte[] buffer):base(ctype,length,buffer)

        {

            Parsing(buffer);

        }

 

        public IEnumerator GetEnumerator()

        {

            return events.GetEnumerator();

        }

 

        private void Parsing(byte[] buffer)

        {

            int offset = 0;

            MDEvent mdevent=null;

            while (offset<buffer.Length)

            {

                mdevent = MDEvent.Parsing(buffer,ref offset,mdevent);

                if(mdevent == null) //분석하지 못한 것이 오면 현재 트랙의 뒷 부분 분석은 Skip

                {

                    break;

                }

                events.Add(mdevent);

            }

        }

    }

}

 


[Proagram.cs]

using System;

using System.IO;

 

namespace 미디_이벤트_분석

{

    class Program

    {

        static string fname = "example.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 Chuck ===");

            int ecnt = 0;

            foreach (MDEvent mdevent in track)

            {

                ecnt++;

                //Console.WriteLine(StaticFuns.HexaString(mdevent.Buffer));

                Console.Write("{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>");

                    Console.WriteLine(StaticFuns.HexaString(mdevent.Buffer));

                }

            }

        }

 

        private static void ViewMeta(MetaEvent metaevent)

        {

            Console.Write("메시지:{0} ", metaevent.Msg);

            Console.Write("길이:{0} ", metaevent.Length);

            Console.WriteLine(metaevent.MetaDescription);

        }

 

        private static void ViewHeader(Header header)

        {

            Console.WriteLine("=== 헤더 Chuck ===");

            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();

        }

    }

}


실습에 사용한 미디 파일

미디 파일.zip


실행 결과를 출력한 파일

result.txt

미디 분석기 실행 화면

미디 분석기 실행 화면



반응형