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

[C#] 미디 분석 프로그램 만들기– 4. 트랙 청크 분석(MTrk) 4.1 delta time 구하기

언휴 2024. 1. 16. 15:47

 

1. 유튜브 동영상 강의

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

2. 트랙 청크의 구조와 delta time

앞에서 미디 파일은 청크의 집합이라는 것과 청크에는 헤더 청크와 트랙 청크가 있다는 것을 확인하였습니다. 또한 헤더 청크의 내용을 분석하는 것까지 살펴보았습니다.

이번에는 트랙 청크를 분석하는 것을 할 거예요. 그리고 제일 먼저 delta time을 구하는 코드를 구현해 볼 거예요.

트랙 청크는 청크 데이터 부분에 트랙 이벤트 정보들로 구성합니다. 트랙 이벤트 정보는 해당 이벤트가 미디가 시작하여 어느 시점에 발생할 이벤트인지를 결정하는 delta time 부분이 먼저 오며 이후에 이벤트 정보가 옵니다.  이벤트 정보는 메타 이벤트, 시스템 이벤트, 미디 이벤트로 구분할 수 있습니다.

미디파일 트랙 청크 구조
미디파일 트랙 청크 구조

먼저 delta time은 이벤트를 발생할 시점 정보를 갖고 있는 필드로 1바이트에서 4바이트까지 차지하는 가변 길이 필드입니다.

delta time을 나타내는 각 바이트의 첫 번째 비트는 다음 바이트도 delta time에 해당하는지 여부(0:No, 1:Yes)를 나타냅니다.

예를 들어 이벤트 필드 값이 16진수로 07 88 AB … 같은 형태로 진행하면 첫 번째 바이트(0x07, 이진수로 0000 0111)의 맨 앞 비트가 0이므로 두 번째 바이트부터는 이벤트 정보입니다. 즉, 첫 번째 바이트인 0x07만 delta time이라는 것입니다.

만약 이벤트 필드 값이 16진수로 CB 73 24 … 같은 형태로 진행하면 첫 번째 바이트(0xCB, 이진수로 1100 1011)의 맨 앞 비트가 1이므로 두 번째 바이트로 delta time 필드입니다. 두 번째 바이트(0x73, 이진수로 0111 0011)의 맨 앞 비트가 0이므로 세 번째 바이트부터는 이벤트 정보입니다. 따라서 첫 번째 바이트의 7비트(100 1011)과 두 번째 바이트의 7비트(111 0011)값을 합친 이진수로 0010 0101 1111 0011 값이 delta time 입니다.

3. Chuck 클래스 추가 구현

Chunk 클래스의 Parse 메서드에 트랙 타입(매직이 MTrk)이 트랙 청크일 때 Track 개체를 생성하여 반환하는 코드를 추가합니다.

        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);
                    case 0x4d54726b: return new Track(ctype, length, buffer);
                }
                return new Chunk(ctype, length, buffer);
            }
            catch
            {
                return null;
            }
        }

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

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);
                    case 0x4d54726b: return new Track(ctype, length, buffer);
                }
                return new Chunk(ctype, length, buffer);
            }
            catch
            {
                return null;
            }
        }
        public override string ToString()
        {
            return CTString;
        }
    }
}

4. Track 클래스 정의

Track 클래스의 접근 지정을 public으로 설정합니다.

그리고 Track 이벤트의 집합인데 구성하는 이벤트 요소를 열거할 수 있게 IEnumerable 인터페이스를 구현 약속합니다.

    public class Track : Chunk,IEnumerable
    {

이벤트 컬렉션을 멤버로 캡슐화합니다.

이벤트 형식은 MDEvent 클래스(아래에 설명)로 정의할게요.

    public class Track : Chunk,IEnumerable
    {
        List<MDEvent> events = new List<MDEvent>();

IEnumerable 인터페이스에 약속한 GetEnumerator 메서드를 구현합니다.

        public IEnumerator GetEnumerator()
        {
            return events.GetEnumerator();
        }

생성자에서 buffer 내용을 분석합니다. 이 부분은 Parsing 메서드를 만들어 정의할게요.

        public Track(int ctype, int length, byte[] buffer) : base(ctype, length, buffer)
        {
            Parsing(buffer);
        }

        private void Parsing(byte[] buffer)
        {

Parsing 메서드에서는 offset이 buffer의 길이보다 작다면 이벤트를 분석하여 events 컬렉션에 추가하는 것을 반복합니다.

이벤트를 분석하는 부분은 이후 강의부터 진행하기 때문에 이번 실습에서는 기본적인 루틴만 구현합니다.

        {
            int offset = 0;
            MDEvent mdevent = null;
            while(offset < buffer.Length)
            {
                mdevent = MDEvent.Parsing(Buffer, ref offset, mdevent);
                if(mdevent == null)
                {
                    break;
                }
                events.Add(mdevent);
            }
        }

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

using System.Collections;
using System.Collections.Generic;

namespace ehmidi
{
    public class Track : Chunk,IEnumerable
    {
        List<MDEvent> events = new List<MDEvent>();
        public IEnumerator GetEnumerator()
        {
            return events.GetEnumerator();
        }
        public Track(int ctype, int length, byte[] buffer) : base(ctype, length, buffer)
        {
            Parsing(buffer);
        }

        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)
                {
                    break;
                }
                events.Add(mdevent);
            }
        }
    }
}

5. MDEvent 클래스 정의

노출 수준을 public으로 접근 지정합니다.

    public class MDEvent
    {

멤버 속성으로 delta 타입, 이벤트 타입, 버퍼를 캡슐화합니다.

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

Parsing 메서드에서 Delta 타임을 구하는 부분을 작성하고 나머지 부분은 차후에 구현할게요.

        public static MDEvent Parsing(byte[] buffer, ref int offset, MDEvent mdevent)
        {
            int oldoffset = offset;
            int delta = StaticFuns.ReadDeltaTime(buffer, ref offset);
            offset = offset;//차후에 수정
            return null;//차후에 구현
        }

현재까지 작성한 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);
            offset = offset;//차후에 수정
            return null;//차후에 구현
        }
    }
}

6. Delta time 구하기

delta time을 구하는 메서드는 StaticFuns 클래스에 정의할게요.

다음은 delta time을 구하는 소스 코드입니다. (여기에서는 buffer의 offset 인덱스부터 delta time값이 있으며 메서드 수행 후에 호출한 곳에서 변화한 offset을 확인할 수 있게 하기 위해 offset을 ref 유형으로 정의하였습니다.)

현재까지 구한 시간을 기억하는 변수가 time입니다.

1바이트 데이터(buffer[offset)를 얻어와서 별도의 변수 b에 설정합니다.

기존 time을 7비트 좌측 쉬프트하고 b의 7비트만 추출하여 둘 값을 합쳐 time에 설정합니다.

이와 같은 작업을 b의 첫 번째 비트가 1(b>127)이면 반복합니다.

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

현재까지 작성한 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;
        }
        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;
        }
    }
}