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

[C#] 미디 분석 프로그램 만들기– 4. 트랙 청크 분석(MTrk) 4.3 Meta Event 상세 구현(키, 박자 정보 등)

언휴 2024. 1. 17. 08:11

1. 유튜브 동영상 강의

미디 분석 프로그램 - 트랙 청크 분석, Meta Event

2. 해야 할 일

이전 글에서 미디 파일의 Track 청크의 메타 데이터에 관해 다루고 이를 분석하는 소스 코드를 소개하였습니다.

그런데 메타 데이터의 내용을 무조건 문자열로 구성하고 있다는 가정으로 정보를 제공하였습니다.

하지만 메타 데이터 내용은 약속된 수치로 특별한 의미를 나타내는 것도 있어요.

대표적으로 박자와 키 정보 등을 들 수가 있어요.

이번에는 메타 데이터의 내용을 상세하게 분석하여 제공하는 기능을 구현하기로 할게요.

3. MetaEvent 클래스 추가 구현

ehmidi 라이브러리의 MetaEvent 클래스에 상세 정보를 제공하는 MetaDescription 속성을 추가합시다.

템포나 박자 등을 계산하는 부분은 별도의 메서드를 만들어 호출하는 형태로 구현할게요.

특별한 사항이 없기 때문에 설명은 생략할게요.

        public string MetaDescription
        {
            get
            {
                switch (Msg)
                {
                    case 0x00: return string.Format("SeqNo:{0}", (Data[0] << 8) | (Data[1]));
                    case 0x01: return "User Message:" + DataString;
                    case 0x02: return "Copyright:" + DataString;
                    case 0x03: return "Track Name:" + DataString;
                    case 0x04: return "Instrument:" + DataString;
                    case 0x05: return "Lyric:" + DataString;
                    case 0x06: return "Marker:" + DataString;
                    case 0x07: return "Cue Point:" + DataString;
                    case 0x08: return "Program Name:" + DataString;
                    case 0x09: return "Device Name:" + 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 "SMPTE Offset: to be defined";
                    case 0x58: return "Time Signature:" + MakeTimeSig();
                    case 0x59: return "Key Scale:" + MakeKeyScale();
                    case 0x7F: return "Specific Message";
                    default: return "ETC";
                }
            }
        }

먼저 키 정보를 정적 멤버로 캡슐화할게요.

keystr은 장조, keystr2는 단조입니다.

keystr, keystr2는 모두 15개의 멤버로 Flat이 7~1개, 아무것도 없을 때, Sharp이 1~7개 있을 때 순입니다.

        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 Flat","C Flat","G Flat","D Flat"
            ,"A",
            "E","B","F Sharp","C Sharp","G Sharp","D Sharp","A Sharp"
        };

MakeKeyScale 메서드를 작성합시다.

첫 번째 바이트는 Key 정보이고 두 번째 바이트는 0일 때 장조, 1일 때 단조입니다.

        private string MakeKeyScale()
        {
            byte ki = (byte)(Data[0] + 7);
            if(Data[1]==0)
            {
                return keystr[ki] + " Major";
            }
            return keystr2[ki] + " minor";
        }

MakeTimeSig 메서드를 구현합시다.

Data[0]은 박자의 분자, Data[1]은 박자의 분모(밑수가 2인 지수)입니다.

Data[2]는 metronome ticks 입니다.

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

MakeTempo 메서드를 구현합시다.

세 개의 바이트 값을 하나의 정수 형식으로 변환합니다.

이 값은 4분 음표를 연주할 시간으로 단위는 micro second입니다.

        private string MakeTempo()
        {
            int tempo = Data[0] << 16 | Data[1] << 8 | Data[2];
            return tempo.ToString() + "microseconds/quarter note";
        }

현재까지 작성한 MetaEvent.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 string MetaDescription
        {
            get
            {
                switch (Msg)
                {
                    case 0x00: return string.Format("SeqNo:{0}", (Data[0] << 8) | (Data[1]));
                    case 0x01: return "User Message:" + DataString;
                    case 0x02: return "Copyright:" + DataString;
                    case 0x03: return "Track Name:" + DataString;
                    case 0x04: return "Instrument:" + DataString;
                    case 0x05: return "Lyric:" + DataString;
                    case 0x06: return "Marker:" + DataString;
                    case 0x07: return "Cue Point:" + DataString;
                    case 0x08: return "Program Name:" + DataString;
                    case 0x09: return "Device Name:" + 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 "SMPTE Offset: to be defined";
                    case 0x58: return "Time Signature:" + MakeTimeSig();
                    case 0x59: return "Key Scale:" + MakeKeyScale();
                    case 0x7F: return "Specific Message";
                    default: return "ETC";
                }
            }
        }

        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 Flat","C Flat","G Flat","D Flat"
            ,"A",
            "E","B","F Sharp","C Sharp","G Sharp","D Sharp","A Sharp"
        };

        private string MakeKeyScale()
        {
            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} metronome:{2}ticks", Data[0], Math.Pow(2, Data[1]), Data[2]);
        }

        private string MakeTempo()
        {
            int tempo = Data[0] << 16 | Data[1] << 8 | Data[2];
            return tempo.ToString() + "microseconds/quarter note";
        }

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

4. 테스트

테스트는 지난 강의에서 사용한 메타 데이터 분석 프로그램을 사용할게요.

대신 ViewMeta 메서드를 다음처럼 수정하세요.

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

테스트에 사용할 Program.cs 소스 코드 내용입니다.

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