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

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

언제나휴일 2018. 5. 1. 16:54
반응형

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




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


이전 글에서 미디 분석 프로그램 프로젝트를 생성하고 자식 컨트롤을 배치했어요.

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

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


이번에는 미디 파일을 열고 청크로 분할하는 작업을 하기로 할게요.


이미 앞에서 소개한 것처럼 미디 파일은 청크들로 이루어져 있습니다. [미디 파일] 미디 파일 구조 분석 및 프로그래밍 1 - 청크 목록


미디 파일 청크 구조

[그림] 청크 구조


먼저 파일 메뉴 아이템에 클릭 이벤트 핸들러를 등록하세요. 

[그림] file 메뉴 아이템의 클릭 이벤트 핸들러 등록


 이제 파일 메뉴 클릭 이벤트 핸들러를 작성합시다.

        private void fileMenuItem_Click(object sender, EventArgs e)

        {

먼저 파일 열기 대화상자(OpenFileDialog) 개체를 생성한 후에 디폴트 확장자를 미디 파일로 명시하고 Filter 속성을 지정하세요.

            OpenFileDialog ofd = new OpenFileDialog();

            ofd.DefaultExt = "미디 파일";

            ofd.Filter = "미디 파일|*.mid"; 

대화 상자를 띄운 후에 결과가 OK라면 파일을 선택한 것입니다.

            if (ofd.ShowDialog() == DialogResult.OK)

            {

                Text = ofd.FileName;

선택한 파일을 입력 인자로 미디 파서(MidiParser) 개체를 생성하세요. 새 클래스 MidiParser를 추가하고 파일 명을 입력 인자로 받는 생성자를 추가하세요.

                MidiParser mp = new MidiParser(ofd.FileName);

미디 파서을 분석하는 과정에 새로운 청크를 발견하면 이를 처리할 이벤트 핸들러를 등록합니다. 청크를 발견하였을 때 처리하는 이벤트 핸들러를 위해 대리자(FindChunkEventHandler)와 이벤트 인자 형식(FindChunkEventArgs)을 추가합니다.

                mp.FindedChuck += Mp_FindedChuck;

미디 파서 개체에게 비동기 방식으로 분석할 것을 명령합니다. 이를 위해 MidiParser 클래스에 AsyncParser 메서드를 추가하세요.

                mp.AsyncParse();

            }

        }


미디 파서에서 미디 파일을 분석하는 중에 청크를 발견하였을 때 이를 처리할 이벤트 핸들러는 다음과 같은 원형을 갖습니다. 이에 관한 코드는 다른 부분을 작성한 후에 구현하기로 할게요.

        private void Mp_FindedChuck(object sender, FindChunkEventArgs e)

        {

        }


먼저 청크를 발견하였을 때의 처리하는 이벤트 핸들러를 위한 대리자와 이벤트 인자 형식을 추가하여 구현합니다.

using System;

namespace 미디_분석_프로그램

{

    public delegate void FindChunkEventHandler(object sender, FindChunkEventArgs e);

이벤트 인자는 기반 형식인 EventArgs에서 파생 형식으로 정의하세요.

    public class FindChunkEventArgs : EventArgs

    {

이벤트 핸들러에서는 발견한 청크를 접근할 수 있어야 할 것입니다. 이를 위해 속성을 캡슐화합니다. 가져오기 속성은 노출하고 설정하기 속성은 내부에서 접근할 수 있게 정하세요. 물론 Chunk 이름의 새 클래스를 생성하세요.

        public Chunk Chunk

        {

            get;

            private set;

        }

생성자에서는 발견한 청크를 입력 인자로 받아 속성을 설정합니다.

        public FindChunkEventArgs(Chunk chuck)

        {

            Chunk = chuck;

        }

    }

}

 


현재 Chunk 클래스는 아무런 멤버도 캡슐화하지 않은 상태입니다.

namespace 미디_분석_프로그램

{

    public class Chunk

    {

    }

}



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

using System.IO;

using System.Threading;

 

namespace 미디_분석_프로그램

{

    public class MidiParser

    {

먼저 미디 파서 개체를 사용하는 곳에서 청크를 발견하였을 때의 이벤트 처리를 할 수 있게 이벤트 멤버를 캡슐화하세요. 

        public event FindChunkEventHandler FindedChuck;

생성할 때 전달받은 파일 이름을 설정하고 필요할 때 가져오기 할 수 있는 파일 이름 속성을 캡슐화하세요.

        public string FileName

        {

            get;

            private set;

        }

생성자는 파일명을 입력인자로 받은 후에 속성을 설정합니다.

        public MidiParser(string fname)

        {

            FileName = fname;

        }

비동기적으로 분석하는 AsyncParse 메서드에서는 분석하는 Parse 메서드를 진입점으로 하는 스레드를 생성한 후에 스레드를 가동합니다.

        public void AsyncParse()

        {

            Thread thread = new Thread(Parse);

            thread.Start();

        }


        public void Parse()

        {

먼저 파일 스트림을 생성합니다.

            FileStream fs = new FileStream(FileName, FileMode.Open)

파일 스트림의 Postion 값이 파일 스트림 길이보다 작다면 반복합니다.

            while (fs.Position < fs.Length)

            {

반복해서 할 일은 파일 스트림에서 청크 하나를 구하는 것입니다. 이를 위해 Chunk 클래스에 Parse 정적 메서드를 추가하세요. 

                Chunk chunk = Chunk.Parse(fs);

그리고 이벤트 멤버가 null 이 아니면 발견한 청크를 이벤트 핸들러로 전달합니다.

                if (FindedChuck != null)

                {

                    FindedChuck(this, new FindChunkEventArgs(chunk));

                }

            }

        }


    }

}



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

using System;

using System.IO;

 

namespace 미디_분석_프로그램

{

    public class Chunk

    {

먼저 헤더 청크를 의미하는 magic과 트랙 청크를 의미하는 magic 상수를 캡슐화합니다.

        const int magic_head = 0x4d546864;

        const int magic_track = 0x4d54726b;

청크 유형을 위한 속성을 캡슐화하세요.

        public int CT//청크 유형

        {

            get;

            private set;

        }

청크 길이를 위한 속성을 캡슐화하세요.

        public int Length//청크 길이

        {

            get;

            private set;

        }

청크의 데이터를 위한 속성을 캡슐화하세요.

        public byte[] Data//데이터

        {

            get;

            private set;

        }

사용자 편의성을 위해 청크 유형을 문자열로 접근할 수 있게 가져오기 속성을 제공합니다. 여기에서는 정수 형식에 있는 magic을 문자열로 변환하는 작업이 필요한데 앞으로 이러한 기능들은 MidiHelper라는 정적 클래스에 정적 메서드로 구현하기로 할게요.

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

        {

            get

            {

                return MidiHelper.GetString(CT);

            }

청크 유형과 길이, 데이터로 구성한 전체 청크의 데이터를 반환하는 속성도 제공합시다.

        public byte[] Buffer

        {

            get

            {

BitConverter 클래스의 정적 메서드 GetBytes를 통해 기본 형식을 byte 배열로 변환할 수 있습니다.

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

길이 부분은 호스트 정렬 방식으로 변환합니다.

                int belen = MidiHelper.ConvertHostorder(Length);

마찬가지로 BitConverter 클래스의 정적 메서드 GetBytes를 통해 기본 형식을 byte 배열로 변환합니다.

                byte[] len_buf = BitConverter.GetBytes(belen);

버퍼를 생성합니다.

                byte[] buffer = new byte[ct_buf.Length + len_buf.Length + Data.Length];

생성한 버퍼에 Array클래스의 Copy 정적 메서드를 이용하여 값을 채우는 작업을 수행합니다.

                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 개체를 생성하여 읽기 작업을 수행할게요.

                BinaryReader br = new BinaryReader(stream);

먼저 4바이트 값을 얻습니다. 청크 유형(magic)이 있는 부분입니다.

                int ctype = br.ReadInt32();

그리고 4바이트 값을 얻습니다. 여기는 청크 데이터 길이에 해당하는 부분인데 호스트 정렬 방식으로 변환하세요. 

                int length = br.ReadInt32(); 

                length = MidiHelper.ConvertHostorder(length);

그리고 데이터 길이 만큼 읽어옵니다.

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

나중에 유형에 따라 헤더 청크 개체를 생성할 것인지 트랙 청크를 생성할 것인지 결정해야 하지만 지금은 그대로 청크 개체를 생성하여 반환합시다.

                return new Chunk(ctype, length, buffer);

            }

            catch

            {

                return null;

            }

        }

        public Chunk(int ctype, int length, byte[] buffer)

        {

            CT = ctype;

            Length = length;

            Data = buffer;

        }

ToString 메서드를 재정의하여 청크 유형과 길이를 표현한 문자열을 반환하게 합시다.

        public override string ToString()

        {

            return string.Format("{0}:{1} bytes", CTString, Length);

        }

    }

}



이번에는 MidiHelper 클래스를 구현합시다.

using System;

using System.Net;

using System.Text;

 

namespace 미디_분석_프로그램

{

    public static class MidiHelper

    {

호스트 정렬로 변환하는 메서드는 이미 IPAddress 클래스에서 제공하는 NetworkToHostOrder 메서드를 호출하는 형태로 래핑합니다.

        public static int ConvertHostorder(int data)

        {

            return IPAddress.NetworkToHostOrder(data);

        }

정수 형식에 있는 매직값을 문자열로 변환하는 부분은 BitConverter 클래스의 GetBytes 메서드로 byte 배열로 변환한 후에 Encoding 개체의 GetString 메서드를 호출합니다.

        public static string GetString(int magic)

        {

            byte[] data = BitConverter.GetBytes(magic);

            Encoding en = Encoding.Default;

            return en.GetString(data);

        }

    }

}



이제 MainForm의 청크를 발견하였을 때의 이벤트 핸들러를 구현합시다.

 

        private void Mp_FindedChuck(object sender, FindChunkEventArgs e)

        {

            AddNode(e.Chunk);

        }

 

 청크를 발견하는 것은 비동기 처리에서 수행하고 있습니다. 따라서 Main Form을 생성한 스레드와 다른 스레드에서 발견하여 크로스 스레드 문제가 발생하므로 다음처럼 크로스 스레드 문제를 해결하는 구조로 작성합니다.

        delegate void AddNodeDele(Chunk chunk);

        private void AddNode(Chunk chunk)

        {

            if (tv_midi.InvokeRequired)

            {

                AddNodeDele dele = AddNode;

                tv_midi.Invoke(dele, new object[] { chunk });

            }

            else

            {

                lbox_chuck.Items.Add(chunk);

            }

        }

미디 분석 프로그램 실행 화면

[그림] 현재까지 작성한 미디 분석 프로그램 실행 화면



▶ MainForm.cs

 using System;

using System.Windows.Forms;

 

namespace 미디_분석_프로그램

{

    public partial class MainForm : Form

    {

        public MainForm()

        {

            InitializeComponent();

        }

 

        private void fileMenuItem_Click(object sender, EventArgs e)

        {

            OpenFileDialog ofd = new OpenFileDialog();

            ofd.DefaultExt = "미디 파일";

            ofd.Filter = "미디 파일|*.mid";

 

            if (ofd.ShowDialog() == DialogResult.OK)

            {

                Text = ofd.FileName;

                MidiParser mp = new MidiParser(ofd.FileName);

                mp.FindedChuck += Mp_FindedChuck;

                mp.AsyncParse();

            }

        }

        private void Mp_FindedChuck(object sender, FindChunkEventArgs e)

        {

            AddNode(e.Chunk);

        }

 

 

        delegate void AddNodeDele(Chunk chunk);

        private void AddNode(Chunk chunk)

        {

            if (tv_midi.InvokeRequired)

            {

                AddNodeDele dele = AddNode;

                tv_midi.Invoke(dele, new object[] { chunk });

            }

            else

            {

                lbox_chuck.Items.Add(chunk);

            }

        }

    }

}

 



▶ FindChunkEventArgs.cs

using System;

 

namespace 미디_분석_프로그램

{

    public delegate void FindChunkEventHandler(object sender, FindChunkEventArgs e);

    public class FindChunkEventArgs : EventArgs

    {

        public Chunk Chunk

        {

            get;

            private set;

        }

        public FindChunkEventArgs(Chunk chuck)

        {

            Chunk = chuck;

        }

    }

}

 


▶ Chunk.cs

using System;

using System.IO;

 

namespace 미디_분석_프로그램

{

    public class Chunk

    {

        const int magic_head = 0x4d546864;

        const int magic_track = 0x4d54726b;

        public int CT//청크 유형

        {

            get;

            private set;

        }

        public int Length//청크 길이

        {

            get;

            private set;

        }

        public byte[] Data//데이터

        {

            get;

            private set;

        }

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

        {

            get

            {

                return MidiHelper.GetString(CT);

            }

        }

        public byte[] Buffer

        {

            get

            {

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

                int belen = MidiHelper.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 = 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: 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;

        }

        public override string ToString()

        {

            return string.Format("{0}:{1} bytes", CTString, Length);

        }

    }

} 


▶ MidiHelper.cs

using System;

using System.Net;

using System.Text;

 

namespace 미디_분석_프로그램

{

    public class MidiHelper

    {

        public static int ConvertHostorder(int data)

        {

            return IPAddress.NetworkToHostOrder(data);

        }

 

        public static string GetString(int magic)

        {

            byte[] data = BitConverter.GetBytes(magic);

            Encoding en = Encoding.Default;

            return en.GetString(data);

        }

    }

} 



▶ MidiParser.cs

 using System.IO;

using System.Threading;

 

namespace 미디_분석_프로그램

{

    public class MidiParser

    {

        public event FindChunkEventHandler FindedChuck;

        public string FileName

        {

            get;

            private set;

        }

        public MidiParser(string fname)

        {

            FileName = fname;

        }

        public void Parse()

        {

            FileStream fs = new FileStream(FileName, FileMode.Open);

            while (fs.Position < fs.Length)

            {

                Chunk chunk = Chunk.Parse(fs);

                if (FindedChuck != null)

                {

                    FindedChuck(this, new FindChunkEventArgs(chunk));

                }

            }

        }

        public void AsyncParse()

        {

            Thread thread = new Thread(Parse);

            thread.Start();

        }

    }

}


반응형