프로젝트/미디 분석 프로그램

[C#] 미디 분석 프로그램 만들기 – 4. 트랙 청크 분석(MTrk) 4.2 Meta Event

언휴 2024. 1. 16. 16:23

1. 유튜브 동영상 강의

미디분석 프로그램 만들기 - 트랙 청크 분석, 미디이벤트

2. Meta Event

이전 강의에서 미디 파일에서 Track 청크의 기본 구조와 delta time을 구하는 것에 관하여 다루었어요.

이번에는 Track 청크의 Meta 이벤트에 관하여 알아보고 분석하는 소스 코드를 소개할게요.

Track 청크의 이벤트는 delta time과 이벤트 정보로 이루어져 있다는 것을 이전 게시글에서 얘기했어요. 또한 이벤트 정보는 Meta 이벤트, System 이벤트, Midi 이벤트 중에 하나라는 것도 소개했었죠.

이벤트 정보가 Meta 이벤트, System 이벤트, Midi 이벤트 중에 어떤 것인지를 확인하려면 delta time 뒤에 오는 첫 번째 바이트 정보의 값을 확인하여야 합니다.

해당 값이 0xFF이면 Meta 이벤트이며 0xF0 ~ 0xFE사이라면 System 이벤트, 0x00~0xEF 사이라면 Midi 이벤트입니다.

여기에서는 Meta 이벤트를 다루고 다른 이벤트는 뒤에서 다루기로 할게요.

Meta 이벤트는 delta time + 0xFF + Meta Data로 구성합니다. 그리고 Meta Data는 0xFF 뒤에 오는 1바이트의 값에 따라 의미가 달라집니다. 다음은 Meta 이벤트에서 delta time 뒤에 오는 값들에 관한 표입니다.

Status byte Second byte Other bytes 설명
0xFF 00 02 ss ss Set track’s sequence #
0xFF 01 nn tt… Any Text user wants
0xFF 02 nn tt… Text for copywright info.
0xFF 03 nn tt… Track name
0xFF 04 nn tt… Track Instrument name
0xFF 05 nn tt.. Lyric
0xFF 06 nn tt… Maker
0xFF 07 nn tt… Cue point
0xFF 20 01 channel Channel prefix
0xFF 21 01 pp MIDI port
0xFF 2F 00 End of Track
0xFF 51 03 tt tt tt Set tempo(microsec/quater note)
0xFF 54 05 hh mm ss ff sfhour/min/sec/frame/subframe SMPTE offset
0xFF 58 04 nn dd cc bbnumerator/demominator/metronome ticks #/32 notes#per quater note Time signature
0xFF 59 02 sf mikey(sharp/flat#)scale(0:major, 1:minor) Key signature(C when Key=0)
0xFF 7F nn tt… Sequencer specific info.

표를 보면 0xFF 뒤에 0x2F가 오면 트랙의 끝을 의미하며 0x2F 뒤에 1바이트까지 Meta Data 필드임을 알 수 있습니다.

그 외에는 delta time + 0xFF + Meta Data 구분자(1바이트) + 길이(1바이트) + 데이터(가변)임을 알 수 있습니다.

3. MDEvent 추가 구현

ehmidilib에 MDEvent.cs 소스 코드를 수정합시다.

MDEvent 클래스에 정적 메서드 Parsing에 메시지 타입이 0xFF일 때 MetaEvent 개체를 생성하여 반환하는 코드를 추가합니다.

        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);
            }
            return null;//차후에 구현
        }

4. MetaEvent 클래스 정의

ehmidilib에 MetaEvent 클래스를 추가합니다. MetaEvent는 MDEvent 기반의 파생클래스입니다.

    public class MetaEvent:MDEvent
    {        

생성자 메서드에서 메시지, 길이, Data를 속성에 설정합니다.

        public MetaEvent(int delta, byte msg, byte len, byte[] data, byte[] orgbuffer) :base(0xFF, delta, orgbuffer)
        {            
            Msg = msg;
            Length = len;
            Data = data;
        }

        public byte Msg
        {
            get;
        }
        public byte Length
        {
            get;
        }
        public byte[] Data
        {
            get;
        }

메타 데이터 내용을 문자열로 변환하여 반환하는 속성을 제공합시다.

        public string DataString
        {
            get
            {
                if(Data == null)
                {
                    return string.Empty;
                }
                return StaticFuns.GetString(Data);
            }
        }

Meta 이벤트 개체를 생성하는 정적 메서드 MakeEvent를 구현합시다.

첫 번째 byte는 Msg입니다.

두 번째 byte는 길이입니다.

나머지는 메타 데이터입니다. (주의할 점은 0x2F일 때는 메타 데이터가 없습니다.)

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

현재까지 작성한 MeteEvent.cs 소스 코드입니다.

using System;

namespace ehmidi
{
    public class MetaEvent:MDEvent
    {        

        public MetaEvent(int delta, byte msg, byte len, byte[] data, byte[] orgbuffer) :base(0xFF, delta, orgbuffer)
        {            
            Msg = msg;
            Length = len;
            Data = data;
        }

        public byte Msg
        {
            get;
        }
        public byte Length
        {
            get;
        }
        public byte[] Data
        {
            get;
        }
        public string DataString
        {
            get
            {
                if(Data == null)
                {
                    return string.Empty;
                }
                return StaticFuns.GetString(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);
        }
    }
}

5. StaticFuns 클래스 추가 구현

메타 데이터를 문자열로 바꾸기 위한 GetString 메서드를 구현합시다.

앞에서 int 형식의 magic을 문자열로 변환하는 메서드도 GetString입니다.

둘은 입력 인자가 다르기 때문에 중복 정의 가능합니다.

메타 데이터는 byte배열로 전달받아 디폴트 인코딩 개체를 이용해 문자열로 변환합니다.

        public static string GetString(byte[] data)
        {
            Encoding en = Encoding.Default;
            return en.GetString(data);
        }

6. 메타 이벤트 분석 프로젝트 만들기

콘솔 앱(.NET Framework) 형태의 프로젝틀 생성합니다.

여기에서는 프로그램 이름을 메타 이벤트 분석이라고 할게요.

ehmidilib를 참조 추가하고 미디 파일을 추가합니다.(https://ehpub.co.kr/files 에 있어요.)

앞에서 만들었던 헤드 청크 분석에 사용한 소스 코드를 복사/붙여넣기 하세요.

그리고 청크가 Track일 때 Track 청크를 보여주는 메서드를 호출하는 구문을 추가합니다.

그리고 Track 청크에서는 이벤트 컬렉션을 보여주는 부분을 구현합니다.

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

        private static void ViewMeta(MetaEvent me)
        {
            Console.Write("메시지:{0} 길이:{1}", me.Msg, me.Length);
            Console.WriteLine(me.DataString);
        }

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