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

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

언제나휴일 2018. 4. 16. 12:13
반응형

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



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


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

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

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

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

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



 Status Byte가 0xFF가 오면 Meta Event라고 하였고 다음에 오는 값에 따라 어떠한 메타 데이터에 관한 것인지를 구분한다고 했었어요. 다음은 앞에서 이미 다루었던 메타 데이터에 관한 표입니다.

Status byte

Meta Data byte

2ndbyte

Other bytes

Description

FF

00

02 ss ss

Set track’s sequence #

FF

01

nn tt…

Any Text user wants

FF

02

nn tt…

Text for copyright info.

FF

03

nn tt…

Track name

FF

04

nn tt…

Track instrument name

FF

05

nn tt…

Lyric

FF

06

nn tt…

Marker

FF

07

nn tt…

Cue point

FF

20

01 channel

Channel prefix

FF

21

01 pp

MIDI port

FF

2F

00

End of Track

FF

51

03 tt tt tt

Set tempo

(microseconds/quarter note)

FF

54

05 hh mm ss ff sf

hour/minute/second

/frame/subframe

SMPTE Offset

FF

58

04 nn dd cc bb

numerator/

denominator/

metronome ticks #/

32nd notes# per quarter note

Time signature

 

FF

59

02 sf mi

key(sharp/flat#)

scale(0:major, 1:minor)

Key signature

(C when Key=0)

FF

7F

7F

Sequencer specific info.


 이번에는 이 중에 0xFF 51로 진행하는 Set tempo 부분과 0xFF 58로 진행하는 Time signature, 0xFF 59로 진행하는 Key signature에 관해 보다 상세하게 설명할게요.


 먼저 0xFF 51로 진행하는 것은 4분 음표를 어느 정도 시간 연주하는지에 관한 값입니다. 

0xFF 51 0x tt tt tt 와 같은 형태로 진행하며 tt 부분은 microseconds를 의미합니다.

예를 들어 0xFF 51 03 07 A1 20로 진행하면 "07A120(500000)microseconds/4분 음표"입니다.

따라서 4분음표를 0.5초 시간동안 연주하라는 것입니다.


 그리고 0xFF 58로 진행하는 박자표와 메트로놈 및 32분 음표가 4분 음표 몇 개로 구성하는지를 나타냅니다.

0xFF 58 04 nn dd cc bb

먼저 nn 부분은 박자표의 분자부분입니다. dd는 박자의 분모 부분을 담당하는데 밑수를 2로하며 dd는 지수부분입니다.

예를 들어 nn이 06이고 dd가 03이면 6/8 박자를 의미합니다. 

cc는 메트로놈 틱 수를 의미하며 bb는 언제나 8로 고정(32분 음표는 4분음표*8이므로)으로 생각할 수 있습니다.

 

 마지막으로 0x0F 59로 진행하는 것은 Key(키)를 표시합니다.

0xFF 59 02 sf mi

sf는 -7에서 7 사이의 값으로 -7은 b이 7개, -6은 b이 6개,..., 0은 기준, 1은 #이 하나, ...입니다.

mi는 0일 때 major, 1일 때 minor입니다.

예를 들어 0x FF 59 02 01 00 이면 #이 하나인 major이므로 G major(사장조)입니다.


다음은 앞에서 작성했던 소스에 이러한 메타 정보를 문자열로 보여주는 부분을 추가 작성한 소스 코드입니다.


[StaticFuns.cs] 공통으로 사용할 함수를 래핑한 정적 클래스

using System;

using System.Net;

using System.Text;

 

namespace 트랙_청크_분석

{

    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 short ConvertHostorderS(byte[] data, int offset)

        {

            return ConvertHostorder(BitConverter.ToInt16(data, offset));

        }

        public static short ConvertHostorder(short data)

        {

            return IPAddress.NetworkToHostOrder(data);

        }

 

        public static string GetString(byte[] data)

        {

            char[] buf = new char[data.Length];

            for (int i = 0; i < data.Length; i++)

            {

                buf[i] = (char)data[i];

            }

            Encoding en = Encoding.Default;

            return en.GetString(data);

        }

 

        public static int ConvertHostorder(int data)

        {

            return IPAddress.NetworkToHostOrder(data);

        }

 

        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;

        }

    }

}

 


[MDEvent.cs]

namespace 트랙_청크_분석

{

    public class MDEvent

    {

        public int Delta

        {

            get;

            private set;

        }

        public byte EventType

        {

            get;

            private set;

        }

        public byte[] Buffer

        {

            get;

            private set;

        }

 

        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)

        {

            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;//미디 이벤트와 시스템 이벤트 부분은 차후에 구현 

        }

    }

}

 


[MetaEvent.cs] - 이번에 수정한 코드가 있는 소스 파일

using System;

 

namespace 트랙_청크_분석

{

    public class MetaEvent:MDEvent

    {

        public byte Msg //어떤 종류의 메타 이벤트인지를 판별

        {

            get;

            private set;

        }

        public byte Length //메타 데이터 길이

        {

            get;

            private set;

        }

        public byte[] Data//메타 데이터

        {

            get;

            private set;

        }

        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}" + BitConverter.ToInt16(Data, 0));

                    case 0x01: return DataString;

                    case 0x02: return "Copyright:" + DataString;

                    case 0x03: return "Track Name:" + DataString;

                    case 0x04: return "Instument:" + DataString;

                    case 0x05: return "Lyric:"+DataString;

                    case 0x06: return "Marker:"+DataString;

                    case 0x07: return "CuePoint:"+DataString;

                    case 0x08: return "ProgramName"+DataString;

                    case 0x09: return "DeviceName"+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 "SmpteOffSet";

                    case 0x58: return "TimeSignature:" + MakeTimeSig(); ;

                    case 0x59: return "KeySignature"+MakeKeySignature();

                    case 0x7F: return "SeqEvent";

                    default: return "ETC";

                }

            }

        }

 

        private string MakeTempo()

        {

            int tempo = Data[0] << 16 | Data[1] << 8 | Data[2];

            return tempo.ToString() + "microseconds/quarter note";

        }

 

        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","C","G","D",

            "A",

            "E","B","F Sharp","C Sharp","G Sharp", "D Sharp", "A Sharp"

        };

        private string MakeKeySignature()

        {           

            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}",Data[0],Math.Pow(2, Data[1]));

        }

 

        public MetaEvent(int delta,byte msg, byte len, byte[] data,byte[] orgbuffer):base(0xFF,delta,orgbuffer)

        {

            Msg = msg;

            Length = len;

            Data = 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);

        }

    }

}

 


[Track.cs]

using System.Collections;

using System.Collections.Generic;

 

namespace 트랙_청크_분석

{

    public class Track:Chunk,IEnumerable

    {

        List<MDEvent> events = new List<MDEvent>();

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

        {

            Parsing(buffer);

        }

 

        public IEnumerator GetEnumerator()

        {

            return events.GetEnumerator();

        }

 

        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) //분석하지 못한 것이 오면 현재 트랙의 뒷 부분 분석은 Skip

                {

                    break;

                }

                events.Add(mdevent);

            }

        }

    }

}

 


[Chunk.cs]

using System;

using System.IO;

 

namespace 트랙_청크_분석

{

    public class Chunk

    {

        public int CT//청크 유형

        {

            get;

            private set;

        }

        public int Length//청크 길이

        {

            get;

            private set;

        }

        public byte[] Data//데이터

        {

            get;

            private set;

        }

        public string CTString//청크 유형(문자열)

        {

            get

            {

                return StaticFuns.GetString(CT);

            }

        }

        public byte[] Buffer

        {

            get

            {

                byte[] ct_buf = BitConverter.GetBytes(CT);

                int belen = StaticFuns.ConvertHostorder(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.ConvertHostorder(length);

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

                int cval = StaticFuns.ConvertHostorder(ctype);

                switch (cval)

                {

                    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 Chunk(int ctype, int length, byte[] buffer)

        {

            CT = ctype;

            Length = length;

            Data = buffer;

        }

    }

}



[Header.cs]

namespace 트랙_청크_분석

{

    public class Header:Chunk

    {

        public int Format//포멧

        {

            get

            {

                return StaticFuns.ConvertHostorderS(Data, 0);

            }

        }

 

        public int TrackCount//트랙 개수

        {

            get

            {

                return StaticFuns.ConvertHostorderS(Data, 2);

            }

        }

 

        public int Division//Division

        {

            get

            {

                /*첫 번째 비트가 1일 때는 다른 코드가 필요함*/

                return StaticFuns.ConvertHostorderS(Data, 4);

            }

        }

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

        {

        }

    }

}

 



[Program.cs]

using System;

using System.IO;

 

namespace 트랙_청크_분석

{

    class Program

    {

        static string fname ="moz_sleep.mid";// "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);

                }

                if (chunk is Track)

                {

                    ViewTrack(chunk as Track);

                }

            }

        }

 

        private static void ViewTrack(Track track)

        {

            Console.WriteLine("=== Track Chuck ===");

            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 metaevent)

        {

            Console.Write("메시지:{0} ", metaevent.Msg);

            Console.Write("길이:{0} ", metaevent.Length);

            Console.WriteLine(metaevent.MetaDescription);

        }

 

        private static void ViewHeader(Header header)

        {

            Console.WriteLine("=== 헤더 Chuck ===");

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

        }

    }

}


사용한 미디파일:

미디 파일.zip


미디파일 분석 실행결과

미디파일 분석 실행결과


반응형