2012/07/05(木)C#でMidiファイルを読む

はてブ数 2012/07/05 21:49 プログラミング::C#つーさ

C#でMidiファイルを読む。

とりあえず、読めればいいや版。

/* MidiFile.cs */

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace Tsukikage.WinMM.MidiIO
{
    public enum EventType
    {
        NoteOff = 0x80,
        NoteOn = 0x90,
        PolyKeyPress = 0xA0,
        ControlChange = 0xB0,
        ProgramChange = 0xC0,
        ChannelPress = 0xD0,
        PichBend = 0xE0,

        SystemExclusive = 0xF0,
        
        SequenceNumber = 0xFF00,
        TextEvent = 0xFF01,
        CopyrightNotice = 0xFF02,
        TrackName = 0xFF03,
        InstrumentName = 0xFF04,
        LyricText = 0xFF05,
        MarkerText = 0xFF06,
        CuePoint = 0xFF07,

        MidiChannelPrefix = 0xFF20,
        EndOfTrack = 0xFF2F,

        TempoSetting = 0xFF51,
        SMTPEOffset = 0xFF54,
        TimeSigunature = 0xFF58,
        KeySigunature = 0xFF59,

        SequencerSpecificEvent = 0xFF7F,
    }

    public class MidiSequence
    {
        #region StreamReader
        static int ReadInt2(Stream s)
        {
            int a = 0;
            a = a << 8 | s.ReadByte();
            a = a << 8 | s.ReadByte();
            return a;
        }

        static int ReadInt3(Stream s)
        {
            int a = 0;
            a = a << 8 | s.ReadByte();
            a = a << 8 | s.ReadByte();
            a = a << 8 | s.ReadByte();
            return a;
        }

        static int ReadInt4(Stream s)
        {
            int a = 0;
            a = a << 8 | s.ReadByte();
            a = a << 8 | s.ReadByte();
            a = a << 8 | s.ReadByte();
            a = a << 8 | s.ReadByte();
            return a;
        }

        static int ReadIntX(Stream s)
        {
            int a = 0;
            while (true)
            {
                int c = s.ReadByte();
                a = a << 7 | (c & 0x7F);
                if ((c & 0x80) == 0)
                    break;
            }
            return a;
        }

        static string ReadText(Stream s)
        {
            int length = ReadIntX(s);
            byte[] buf = new byte[length];
            s.Read(buf, 0, length);
            return Encoding.GetEncoding(932).GetString(buf, 0, length);
        }

        static byte[] ReadSysEx(Stream s)
        {
            int length = ReadIntX(s);
            byte[] buf = new byte[length+1];
            buf[0] = 0xF0; s.Read(buf, 1, length);
            return buf;
        }

        static void ReadUnknownMeta(Stream s)
        {
            int length = ReadIntX(s);
            byte[] buf = new byte[length];
            s.Read(buf, 0, length);
        }

        #endregion

        public string Title = null, Copyright = null;
        public int Tracks;
        public int TimeBase;

        public MidiSequence(Stream source)
        {
            Load(source);
        }

        public MidiSequence(string path)
        {
            using (FileStream fs = File.OpenRead(path))
            {
                Load(fs);
            }
        }

        void Load(Stream fs)
        {
            List<string> stringTable = new List<string>(256);
            List<byte[]> sysexTable = new List<byte[]>(256);
            List<MidiEvent> eventList = new List<MidiEvent>(65536);

            { // Read HEADER
                if (ReadInt4(fs) != 0x4D546864) /* MThd */
                {
                    byte[] aaa = new byte[256];
                    fs.Read(aaa, 0, 16); /* RIFF MIDI */
                    if (ReadInt4(fs) != 0x4D546864) /* MThd */
                    {
                        fs.Read(aaa, 0, 108); /* MAC BINARY */
                        if (ReadInt4(fs) != 0x4D546864) /* MThd */
                            throw new ArgumentException("SMF/RMIじゃないっぽい", "path");
                    }
                }

                int len = ReadInt4(fs);
                int fmt = ReadInt2(fs);
                Tracks = ReadInt2(fs);
                TimeBase = ReadInt2(fs);
            }

            for (int track = 0; track < Tracks; track++)
            {
                if (ReadInt4(fs) != 0x4D54726B) /* MTrk */
                    throw new ArgumentException("SMFが壊れてるか対応してないっぽです。", "path");

                int end = ReadInt4(fs) + (int)fs.Position;

                int currentPos = 0;
                int lastStatus = 0;
                while (fs.Position < end)
                {
                    currentPos += ReadIntX(fs); // deltaTime

                    int first = -1, second = -1, third = -1;

                    first = fs.ReadByte();
                    if ((first & 0xF0) == 0)
                    {
                        // running status
                        second = first;
                        first = lastStatus;
                    }

                    lastStatus = first;
                    int eventType = first & 0xF0;

                    if (eventType != 0xF0)
                    {
                        // short message
                        second = second != -1 ? second : fs.ReadByte();
                        if (eventType != 0xC0) third = fs.ReadByte();
                        else third = 0;
                        eventList.Add(new MidiEvent(currentPos, (EventType)eventType, first, second, third));
                    }
                    else if (first == 0xF0 || first == 0xF7)
                    {
                        // long message
                        byte[] ex = ReadSysEx(fs);
                        eventList.Add(new MidiEvent(currentPos, EventType.SystemExclusive, sysexTable.Count));
                        sysexTable.Add(ex);
                    }
                    else if (first == 0xFF)
                    {
                        // meta event
                        EventType metatype = (EventType)(0xFF00 | fs.ReadByte());
                        switch (metatype)
                        {
                            case EventType.TextEvent:
                            case EventType.CopyrightNotice:
                            case EventType.TrackName:
                            case EventType.LyricText:
                            case EventType.MarkerText:
                            case EventType.CuePoint:
                                string text = ReadText(fs);
                                eventList.Add(new MidiEvent(currentPos, metatype, stringTable.Count));
                                if ((EventType)metatype == EventType.CopyrightNotice) Copyright = text;
                                if ((EventType)metatype == EventType.TrackName && Title == null) Title = text;
                                stringTable.Add(text);
                                break;

                            case EventType.TempoSetting:
                                ReadIntX(fs);
                                eventList.Add(new MidiEvent(currentPos, metatype, ReadInt3(fs)));
                                break;
                            case EventType.TimeSigunature:
                                ReadIntX(fs);
                                eventList.Add(new MidiEvent(currentPos, metatype, ReadInt4(fs)));
                                break;
                            case EventType.KeySigunature:
                                ReadIntX(fs);
                                eventList.Add(new MidiEvent(currentPos, metatype, ReadInt2(fs)));
                                break;

                            default:
                                ReadUnknownMeta(fs);
                                break;
                        }
                    }
                }
            }

            // All Tracks was readed.
            Events = eventList.ToArray();
            MargeSort(Events, delegate(MidiEvent x, MidiEvent y) { return x.Position - y.Position; });
            StringTable = stringTable.ToArray();
            SysExTable = sysexTable.ToArray();
        }

        /// <summary>
        /// 入力されたノートオンイベントに対するノートオフの位置を検索する。
        /// </summary>
        /// <param name="noteOnEvent">noteOnEvent</param>
        /// <returns>位置</returns>
        public int GetNoteOffEventIndex(int noteOnEvent)
        {
            int i = noteOnEvent + 1;
            if (Events[noteOnEvent].EventType != EventType.NoteOn)
                new ArgumentException("NoteOnイベントのインデックスではないようです。", "noteOnEvent");

            int noteCh = Events[noteOnEvent].Channel;
            int noteNum = Events[noteOnEvent].NoteNumber;
            int noteVel = Events[noteOnEvent].NoteVelocity;
            for (; i < Events.Length; i++)
            {
                MidiEvent e = Events[i];
                if (e.EventType == EventType.NoteOff && e.Channel == noteCh && e.NoteNumber == noteNum)
                    return i;
                if (e.EventType == EventType.NoteOn && e.Channel == noteCh && e.NoteNumber == noteNum && e.NoteVelocity == 0)
                    return i;
            }
            return -1;
        }

        public MidiEvent[] Events;
        public String[] StringTable;
        public byte[][] SysExTable;

        static void MargeSort<T>(T[] target, Comparison<T> comparer)
        {
            T[] work = new T[target.Length];
            for (int i = 0; ; i++)
            {
                int n = 1 << i;
                int m = n * 2;
                for (int j = 0; ; j++)
                {
                    int p = m * j;
                    int p1 = p, e1 = p1 + n;
                    int p2 = e1, e2 = Math.Min(p2 + n, target.Length);
                    int seg = e2 - p1;

                    if (seg <= n) break;

                    for (int k = 0; k < seg; k++)
                    {
                        if (p2 >= e2) work[k] = target[p1++];
                        else if (p1 >= e1) work[k] = target[p2++];
                        else if (comparer(target[p1], target[p2]) <= 0) work[k] = target[p1++];
                        else work[k] = target[p2++];
                    }

                    Array.Copy(work, 0, target, p, seg);
                }
                if (n >= target.Length) break;
            }
        }
    }

    public struct MidiEvent
    {
        public MidiEvent(int position, EventType type, int data)
        {
            this.position = position;
            this.type = type;
            this.data = data;
            this.UserData = 0;

            if (this.type == EventType.NoteOn && NoteVelocity == 0)
            {
                this.data ^= 0x10;
                this.type = EventType.NoteOff;
            }

        }

        public MidiEvent(int position, EventType type, int first, int second, int third)
        {
            this.position = position;
            this.type = type;
            this.data = first | second << 8 | third << 16;
            this.UserData = 0;

            if (this.type == EventType.NoteOn && NoteVelocity == 0)
            {
                this.data ^= 0x10;
                this.type = EventType.NoteOff;
            }
        }

        int position;
        EventType type;
        int data;
        public int UserData;

        public int Position { get { return position; } }
        public EventType EventType { get { return type; } }

        public int Channel { get { return data & 0x0F; } }

        public int NoteNumber { get { return data >> 8 & 0xFF; } }
        public int NoteVelocity { get { return data >> 16 & 0xFF; } }

        public int ControlNumber { get { return data >> 8 & 0xFF; } }
        public int ControlValue { get { return data >> 16 & 0xFF; } }
        public int ProgramNumber { get { return data >> 8 & 0xFF; } }

        public int PichBend { get { return (data >> 1 & 0x3F80) | (data >> 16 & 0x7F); } }

        public int Tempo { get { return data; } }
        public double BPM { get { return 60000000.0 / data; } }

        public int TimeNum { get { return data >> 24 & 0xFF; } }
        public int TimeDenom { get { return data >> 16 & 0xFF; } }
        public int TimeBeat { get { return data >> 8 & 0xFF; } }

        public int ScaleSharps { get { return data >> 8 & 0xFF; } }
        public int ScaleMajor { get { return data & 0xFF; } }

        public int SysExIndex { get { return data; } }
        public int TextIndex { get { return data; } }

        public uint RawData { get { return (uint)data; } }

        public override string ToString()
        {
            string pos = "" + (position / 1920 + 1).ToString("D4") + "." + (position % 1920 / 480 + 1) + "." + (position % 480).ToString("D3") + " > ";
            switch (EventType)
            {
                case EventType.NoteOn: return pos + "NoteOn : Ch." + Channel + " N" + NoteNumber + " V" + NoteVelocity;
                case EventType.NoteOff: return pos + "NoteOff : Ch." + Channel + " N" + NoteNumber;
                case EventType.ProgramChange: return pos + "ProgChg : Ch." + Channel + " Prog." + ProgramNumber;
                case EventType.ControlChange: return pos + "CtrlChg : Ch." + Channel + " CC" + ControlNumber + ">" + ControlValue;
                case EventType.PichBend: return pos + "PichBend : Ch." + Channel + " PB." + PichBend;
                case EventType.ChannelPress: return pos + "ChannelPress : Ch." + Channel;
                case EventType.PolyKeyPress: return pos + "PolyKeyPress : Ch." + Channel + " N" + NoteNumber + " V" + NoteVelocity;

                case EventType.SystemExclusive: return pos + "SysEx : " + data;

                case EventType.TempoSetting: return pos + "Tempo : " + data + "μs/beat = BPM. " + BPM + ")";
                case EventType.TimeSigunature: return pos + "TimeSign : " + TimeNum + " / " + "2^" + TimeDenom;
                case EventType.KeySigunature: return pos + "KeySign : Sharps." + ScaleSharps + " " + (ScaleMajor == 1 ? "Major" : "Minor");
                case EventType.TextEvent: return pos + "Text : " + data;
                case EventType.TrackName: return pos + "TrackName : " + data;
                case EventType.CopyrightNotice: return pos + "Copyright : " + data;
                case EventType.InstrumentName: return pos + "InstName : " + data;
                case EventType.LyricText: return pos + "Lyric : " + data;
                case EventType.MarkerText: return pos + "Marker : " + data;
                case EventType.CuePoint: return pos + "CuePoint : " + data;
                default: return pos + EventType.ToString() + " : " + data;
            }
        }
    }
}