미디 분석 프로그램 – 3. Head 청크, MThd

안녕하세요. 언제나 휴일입니다.

1. 해야 할 일과 이론

이전 글에서 미디 파일은 청크들의 집합이라는 얘기와 함께 이를 확인하는 C# 소스 코드를 소개하였습니다.

이번에는 미디 파일의 청크 중에 Head 청크의 구조를 알아보고 이를 분석하는 C# 소스 코드를 소개할게요.

미디 파일의 head 청크는 14 바이트로 구성합니다.

청크 타입부분의 값은 ASCII 코드에서 Mthd에 해당하는 값(16진수로 4D 54 68 64)이 옵니다.

헤드 청크의 길이는 6바이트입니다.

Head 청크의 데이터는 포멧, 트랙 개수, division으로 구성합니다.

포멧은 2바이트 차지하며 0,1,2 중에 하나입니다.

0: 단일 트랙,  1: 다중 트랙(여러 악기를 표현하기 쉬움), 2: 다중 트랙(트랙마다 시퀀스를 포함할 수 없는 제약이 있음)

트랙 개수는 2바이트를 차지합니다.

division은 2바이트를 차지합니다. division은 1delta time이 어느 정도의 길이를 의미하는지를 나타냅니다.

delta time에 관한 사항은 다음 강의에서 다룰 거예요.

2. ehmidilib 추가

클래스 라이브러리(.NET Framewok)프로젝트 ehmidilib를 추가하세요.

프로젝트에 이전 프로젝트에서 만든 Chunk.cs와 StaticFuns.cs를 추가합니다.

namespace 부분은 ehmidi로 변경합니다.

3. StaticFuns 추가 구현

2바이트 데이터를 호스트 바이트 오더로 변환하는 메서드를 추가합니다.

        public static short ConvertHostorder(short data)
        {
            return IPAddress.NetworkToHostOrder(data);
        }
        public static short ConvertHostorderS(byte[] data,int offset)
        {
            return ConvertHostorder(BitConverter.ToInt16(data, offset));
        }

byte 배열의 내용을 Hexa 코드로 볼 수 있게 Hexa 코드 문자열로 변환하는 메서드를 추가합니다.

        public static string HexaString(byte[] buffer)
        {
            string str = "";
            foreach(byte d in buffer)
            {
                str += string.Format("{0:X2} ", d);
            }
            return str;
        }

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

using System;
using System.Net;
using System.Text;

namespace ehmidi
{
    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 int ConverHostorder(int data)
        {
            return IPAddress.NetworkToHostOrder(data);
        }
        public static short ConvertHostorder(short data)
        {
            return IPAddress.NetworkToHostOrder(data);
        }
        public static short ConvertHostorderS(byte[] data,int offset)
        {
            return ConvertHostorder(BitConverter.ToInt16(data, offset));
        }
        public static string HexaString(byte[] buffer)
        {
            string str = "";
            foreach(byte d in buffer)
            {
                str += string.Format("{0:X2} ", d);
            }
            return str;
        }
    }
}

4. Chuck 클래스 추가 구현

Parse 정적 메서드에서 읽어온 청크 타입이 헤더 청크(0x4d546864, MThd)일 때 Header 개체를 생성하는 코드를 작성합니다.

물론 Header 클래스를 프로젝트에 추가해야 합니다. Header 클래스 부분은 바로 이어서 다룰거예요.

        public static Chunk Parse(Stream stream)
        {
            try
            {
                BinaryReader br = new BinaryReader(stream);
                int ctype = br.ReadInt32();
                int length = br.ReadInt32();
                length = StaticFuns.ConverHostorder(length);
                byte[] buffer = br.ReadBytes(length);
                switch(StaticFuns.ConverHostorder(ctype))
                {
                    case 0x4d546864: return new Header(ctype, length, buffer);
                }
                return new Chunk(ctype, length, buffer);
            }
            catch
            {
                return null;
            }
        }

Chuck클래스에 원본 버퍼를 반환하는 속성을 제공합시다.

청크 타입, 길이, Data를 하나의 버퍼에 복사하여 반환하게 구현합니다.

        public byte[] Buffer
        {
            get
            {
                byte[] ct_buf = BitConverter.GetBytes(CT);
                int belen = StaticFuns.ConverHostorder(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;
            }
        }

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

using System;
using System.IO;

namespace ehmidi
{
    public class Chunk
    {

        public int CT
        {
            get;
        }
        public int Length
        {
            get;
        }
        public byte[] Data
        {
            get;
        }
        public Chunk(int ctype, int length, byte[] buffer)
        {
            CT = ctype;
            Length = length;
            Data = buffer;
        }
        public string CTString
        {
            get
            {
                return StaticFuns.GetString(CT);
            }
        }
        public byte[] Buffer
        {
            get
            {
                byte[] ct_buf = BitConverter.GetBytes(CT);
                int belen = StaticFuns.ConverHostorder(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.ConverHostorder(length);
                byte[] buffer = br.ReadBytes(length);
                switch(StaticFuns.ConverHostorder(ctype))
                {
                    case 0x4d546864: return new Header(ctype, length, buffer);
                }
                return new Chunk(ctype, length, buffer);
            }
            catch
            {
                return null;
            }
        }
        public override string ToString()
        {
            return CTString;
        }
    }
}

5. 헤드 청크 분석 코드 작성

콘솔 앱(.NET Framework) 프로젝트 헤드 청크 분석을 추가합니다.

프로젝트에 ehmidilib를 참조 추가합니다. (미디 샘플 파일도 필요합니다.)

Chunk 목록을 분석하는 코드 부분에서 Chunk가 Header일 때 헤더 청크 정보를 출력하게 수정합니다.

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

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