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

[미디 분석 프로그램 만들기] 5. 트랙 청크 분석하기 - delta time 구하기

언제나휴일 2018. 5. 3. 10:54
반응형

[미디 분석 프로그램 만들기] 5. 트랙 청크 분석하기 - delta time 구하기



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


앞에서 미디 파일을 청크 단위로 분할하여 리스트 박스에 표시하는 것과 헤더 청크를 분석하는 부분을 다루었습니다.

[미디 분석 프로그램 만들기] 1. 구현할 프로그램 소개

[미디 분석 프로그램 만들기] 2. 프로젝트 생성 및 Layout

[미디 분석 프로그램 만들기] 3. 미디 파일 열기 및 청크로 분할하기

[미디 분석 프로그램 만들기] 4. 헤더 청크 분석하기


이번에는 트랙 청크를 분석하는 부분을 다룰 것입니다. 트랙 청크는 트랙 이벤트들로 구성하며 트랙 이벤트는 Meta 이벤트 System 이벤트, Midi 이벤트가 있습니다. 그리고 트랙 이벤트는 해당 이벤트가 언제 발생하는 이벤트인지를 결정하는 delta time(가변 길이, 1~4바이트)이 오며 이어서 이벤트 데이터(Meta 이벤트 or System 이벤트 or Midi 이벤트)가 따라 옵니다. 여기에서는 delta time을 계산하는 방법을 구현합시다.

자세한 사항은 다음 게시글을 참고하세요.

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

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

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

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

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

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

[미디 파일] 미디 파일 구조 분석 및 프로그래밍 7 - 컨트롤 번호

[미디 파일] 미디 파일 구조 분석 및 프로그래밍 8 -악기 번호

[미디 파일] 미디 파일 구조 분석 및 프로그래밍 9 - 시스템 이벤트


트랙 청크 구조

[그림] 트랙 청크 구조


트랙 청크는 위 그림처럼 magic(MTrk)부분과 길이 뒤에 이벤트들로 구성합니다.

그리고 이벤트는 delta 타임과 이벤트 데이터(Meta or System or Midi)로 구성합니다. 

이 중에 delta 타임은 이벤트를 발생할 시점 정보를 갖고 있는 필드로 1에서 4바이트 사이의 가변 길이 필드입니다.

delta 타임을 나타내는 바이트의 첫 번째 비트 값이 다음 바이트도 delta 타임을 나타내는 바이트인지 여부(0:No, 1:Yes)를 나타냅니다.

자세한 사항은 [미디 파일] 미디 파일 구조 분석 및 프로그래밍 3 - Track 청크 1, delta time 구하기 글을 참고하세요.



이제 앞에서부터 만들고 있는 미디 분석 프로그램을 구현합시다. 제일 먼저 Chunk 클래스의 Parse 메서드에 청크 유형을 나타내는 magic이 트랙 청크를 의미할 때 청크 개체를 생성하여 반환하는 코드를 추가하세요.

 

        public static Chunk Parse(Stream stream)

        {

            try

            {

                BinaryReader br = new BinaryReader(stream);

                int ctype = br.ReadInt32();

                int length = br.ReadInt32();

                length = MidiHelper.ConvertHostorder(length);

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

                int cval = MidiHelper.ConvertHostorder(ctype);

                switch (cval)

                {

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

다음의 case magic_track: 부분이 추가한 코드입니다.

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

                }

                return new Chunk(ctype, length, buffer);

            }

            catch

            {

                return null;

            }

        }



이제 Track 클래스를 구현합시다.


using System.Collections;

using System.Collections.Generic;

 

namespace 미디_분석_프로그램

{

Track 클래스는 이벤트들로 구성하고 있습니다. 따라서 Track 개체를 구성하는 이벤트를 열거할 수 있게 IEnumerable 인터페이스를 기반으로 구현 약속할게요.

    public class Track : Chunk, IEnumerable

    {

여기에서는 트랙을 구성하는 이벤트를 컬렉션에 보관할 거예요. 특히 작성하는 미디 분석 프로그램에서는 트리 뷰에 이벤트 항목을 선택하면 헥사 코드에서 어느 영역인지 마킹하여 확인하기 쉽게 보여주는 기능을 제공할 것입니다. 이를 위해서 해당 이벤트가 트랙 청크의 어느 위치에 있고 크기가 얼마인지 알 수 있어야 하는데 이 정보를 Pair 이름의 클래스로 정의할 것입니다.


따라서 트랙을 구성하는 이벤트(MDEvent)와 청크 트랙에서의 위치와 크기(Pair)를 사전 개체에 보관합시다. 이를 위한 필드를 선언하고 사전 개체를 생성하세요.

        Dictionary<MDEvent, Pair> mdic = new Dictionary<MDEvent, Pair>();


생성자에서 트랙을 분석하는 메서드를 호출할게요.

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

        {

            Parsing(buffer);

        }

 IEnumerable 인터페이스에 약속한 GetEnumerator 메서드를 구현합시다. 여기에서는 사전 개체의 Key(MDEvent)를 보관하는 Keys 컬렉션을 열거할 수 있게 Keys컬렉션의 GetEnumerator 메서드를 호출한 결과를 bypass합니다.

        public IEnumerator GetEnumerator()

        {

            return mdic.Keys.GetEnumerator();

        }

 

이제 트랙 청크를 분석하는 Parsing 메서드를 구현합시다.

        private void Parsing(byte[] buffer)

        {

            int offset = 0;

            MDEvent mdevent = null;

            int old;

offset이 buffer 길이보다 작다면 분석을 계속합니다.

            while (offset < buffer.Length)

            {

Pair 개체는 해당 이벤트의 시작 위치와 길이를 알아야 합니다. offset을 이동하면서 다음 이벤트 위치까지 분석하기 때문에 이동하기 전 위치를 기억합니다.

                old = offset;

MDEvent의 정적 메서드 Parsing에서 이벤트 하나를 분석하게 합시다. offset은 분석한 만큼 이동하며 이를 호출한 이 곳에서 알아야 하므로 ref 방식의 입력 인자로 전달합니다. 그리고 여기에서 소개하지는 않았지만 미디 이벤트에는 Running Status라는 것이 있는데 이전 미디 이벤트의 상태와 같다는 것입니다. 이를 위해 이전 이벤트도 입력 인자로 전달할게요.

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

기억해 놓았던 offset(old 값)과 이벤트 하나를 분석한 후의 offset값을 전달하여 Pair 개체를 생성합니다.

                Pair pair = new Pair(old, offset);

사전 개체에 분석한 이벤트 개체와 사전 개체를 매핑합니다.

                mdic[mdevent] = pair;

만약 이벤트를 분석하지 못하였을 때는 반복문을 탈출하는 형태로 구현하기로 할게요.

                if (mdevent == null)

                {

                    break;

                }

            }

        }

        public override string ToString()

        {

            return string.Format("<트랙 청크> " + base.ToString());

        }

이벤트와 대응하는 Pair 개체를 검색하여 반환하는 메서드를 제공합시다.

        public Pair FindPair(MDEvent md)

        {

            if (mdic.ContainsKey(md))

            {

                return mdic[md];

            }

            return null;

        }

    }

}



이제 MDEvent 클래스를 구현합시다.

 

using System;

 

namespace 미디_분석_프로그램

{

 

    public class MDEvent

    {

이벤트가 발생하는 시점을 알 수 있는 Delta 속성을 캡슐화하세요.

        public int Delta

        {

            get;

            private set;

        }

이벤트 유형을 구분하는 속성을 캡슐화하세요. 

        public byte EventType

        {

            get;

            private set;

        }

이벤트 데이터 속성을 캡슐화하세요.

        public byte[] Buffer

        {

            get;

            private set;

        }

 이벤트 유형과 delta 값과 이벤트 데이터를 입력 인자로 받는 생성자를 정의하세요. 

        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)

        {

offset값을 기억합니다.

            int oldoffset = offset;

delta 타임을 계산합니다. 이 부분은 MidiHelper 클래스에 정적 메서드(ReadDeltaTime)로 구현합시다.

            int delta = MidiHelper.ReadDeltaTime(buffer, ref offset);

delta 타임 뒤에 오는 1바이트의 값에 따라 이벤트 종류를 결정합니다. 이 부분은 다음에 메타 이벤트와 미디 이벤트와 시스템 이벤트를 분석하는 코드를 작성할 부분입니다.

            return null; 

        }

    }

}



MidiHelper 클래스에 delta 타임을 계산하는 정적 메서드를 구현합시다.

 

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

        {

            int time = 0;

            byte b;

현재 offset값을 얻어옵니다. 그리고 이전까지 계산한 값을 7비트 왼쪽으로 이동하고 현재 offset에 있는 값의 하위 7비트(0x7F와 &마스크 한 값)값을 | 마스크 한 값이 현재까지의 delta 타임입니다.

이러한 작업은 현재 offset 값이 127보다 크면(첫번째 비트가 1) 반복합니다.

            do

            {

                b = buffer[offset];

                offset++;

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

            } while (b > 127);

            return time;

        }



반응형