カクタスソフトウェア
カクタスソフトウェア
サウンド MIDI マルチメディア アプリケーション

クラスを用いたMIDIストリームの実装

このセクションでは、クラスを用いたMIDIストリームの実装法について解説します。

ストリームは、MIDIイベントの記録とイベントの操作を支援するクラスです。設計するシーケンサーの機能によって内容は異なりますが、基本的に以下のプロパティやメソッドを実装すれば良いでしょう。

プロパティ

メソッド

Streamクラスを作成し、プロパティやメソッドを含めます。コンストラクタでは、リストとイベント位置の初期化を行います。

class CStream
{
public:
    CStream(void);
    virtual ~CStream(void);

public:
    // プロパティ
    ・・・
public:
    // メソッド
    ・・・
};
CStream::CStream(void)
{
    SeekTop();
}

CStream::~CStream(void)
{
}
    public class Stream
    {
        // プロパティ
        ・・・
        // メソッド
        ・・・
        public Stream()
        {
            FormList = new List<Form>();
            SeekTop();
        }
    }

プロパティ

public:
    CArray <CForm, CForm&>  m_FormList;
    int                     m_Position;

public:
    inline int NumForm()
    {
        return m_FormList.GetCount();
    }
        public List<Form>   FormList { get; private set; }
        public int          NumForm { get { return FormList.Count; }}
        public int          Position { get; set; }

FormListは、MIDIイベントの基底クラスをメンバーとする配列です。C++ではCArrayを用い、C#ではリストを用いました。この実装では簡単のため、一つの配列ですべてのイベントを保持しています。そのため、イベント数が多くなると、動作が遅くなる可能性があります。

NumFormは、全イベントの数を戻します。

Positionは、現在読み出し中、あるいは挿入中のイベントの位置を保持します。

メソッド

メソッドはいくつかの機能グループに分けることができます。

イベント位置に関連する機能

ストリームの先頭は、一番上のイベントよりさらに一つ上のポジションを示すようにします。これは、イベントを挿入するとき、一番上に挿入できるようにするためと、イベントが一件も無い場合でも、矛盾なく状態を表現できるようにするためです。

また最後尾の位置は、最後のイベントの次のポジションを示すようにします。これもイベントが一件も無い場合に対応するためです。

イベントが一件も無い場合、先頭位置は「-1」に、最後尾は「0」になります。

以下に先頭と最後尾へ移動するコード例を示します。

void CStream::SeekTop()
{
    m_Position = -1;
}

void CStream::SeekBottom()
{
    m_Position = NumForm();
}
        public void SeekTop()
        {
            Position = -1;
        }

        public void SeekBottom()
        {
            Position = NumForm;
        }

任意のイベント位置への移動は、範囲チェックを行います。先頭より小さいときは、TopEndを戻し、最後尾より大きいときは、BottomEndを戻します。

Error CStream::SeekPos(int pos)
{
    Error err = QueryPos(pos);

    switch (err)
    {
    case NoErr:
        m_Position = pos;
        break;
    case TopEnd:
        m_Position = -1;
        break;
    case BottomEnd:
        m_Position = NumForm();
        break;
    }
    return err;
}
        public Error SeekPos(int pos)
        {
            Error err = QueryPos(pos);

            switch (err)
            {
            case Error.NoErr:
                Position = pos;
                break;
            case Error.TopEnd:
                Position = -1;
                break;
            case Error.BottomEnd:
                Position = NumForm;
                break;
            }
            return err;
        }

QueryPos()は、イベント位置の範囲チェックを行うメソッドです。

Error CStream::QueryPos(int pos)
{
    if (pos < 0)
    {
        return TopEnd;
    }
    if (NumForm() <= pos)
    {
        return BottomEnd;
    }
    return NoErr;
}
        public Error QueryPos(int pos)
        {
            if (pos < 0)
            {
                return Error.TopEnd;
            }
            if (NumForm <= pos)
            {
                return Error.BottomEnd;
            }
            return Error.NoErr;
        }

イベントの位置を指定するには、目的のイベントが配列の何番目に記録されているかを指定する「配列の位置」による方法と、イベントに記録されているチック位置を手がかりに検索する「チック位置」による方法があります。

配列の位置を指定した場合、イベントの場所は一意に決まります。しかし、チック位置で指定した場合は、注意が必要です。もし同一チックのイベントが一つ以上ある時、対象になる位置が複数存在するため、イベントの上側か下側かを指定しなければなりません。

例として1920チックへ移動する場合を考えます。

ケース1

該当するイベントが1つ見つかった場合。

○印は、見つかったイベントを示します。また◎印は、一つ前のイベントを示します。

チック位置 カレント位置
960
1920
2880  

ケース2

該当するイベントが見つからなかった場合。目的のチック位置を越えない範囲で最も近いイベントに移動します。ここでは、1440チックが該当します。

チック位置 カレント位置
960  
1440 ○◎
2880  

ケース3

該当するイベントが複数見つかった場合。○印は、見つかったイベントで最も下側のイベントを示します。◎印は、最も上側のイベントの一つ前のイベントを示します。

チック位置 カレント位置
960
1920  
1920  
1920
2880  

◎印は、指定したチック位置の先頭にイベントを挿入するとき用います。一方、○印は、チック位置の最後に挿入するとき用います。シークコマンドは、◎印と○印に対応する、二つのバージョンがあると便利です。

同一チックのイベントが無ければ、◎印と○印どちらも結果は同じです。

以下に、チック位置を手がかりに目的のイベント位置に移動するメソッドを示します。二分法の手法を用いているため、イベント数が多くても高速に動作します。SeekTopTick()は、◎印の位置へ移動し、SeekLastTick()は、○印の位置へ移動します。

Error CStream::SeekTopTick(int tickPos)
{
    int left  = 0;
    int right = NumForm();

    while (left < right)
    {
        int mid = (left + right) / 2;
        CForm form = m_FormList[mid];
        if (form.m_TickPos < tickPos)   left  = mid + 1;
        else                            right = mid;
    }
    return SeekPos(left - 1);
}

Error CStream::SeekLastTick(int tickPos)
{
    int left  = 0;
    int right = NumForm();

    while (left < right)
    {
        int mid = (left + right) / 2;
        CForm form = m_FormList[mid];
        if (form.m_TickPos <= tickPos)  left  = mid + 1;
        else                            right = mid;
    }
    return SeekPos(left - 1);
}
        public Error SeekTopTick(int tickPos)
        {
            int left  = 0;
            int right = NumForm;

            while (left < right)
            {
                int mid = (left + right) / 2;
                Form form = FormList[mid];
                if (form.TickPos < tickPos)     left  = mid + 1;
                else                            right = mid;
            }
            return SeekPos(left - 1);
        }

        public Error SeekLastTick(int tickPos)
        {
            int left  = 0;
            int right = NumForm;

            while (left < right)
            {
                int mid = (left + right) / 2;
                Form form = FormList[mid];
                if (form.TickPos <= tickPos)    left  = mid + 1;
                else                            right = mid;
            }
            return SeekPos(left - 1);
        }

イベントの挿入・削除に関連する機能

挿入

以下の図は、カレント位置に新しいイベントを挿入する様子を示しています。左が挿入前、右が挿入後です。新しいイベントが挿入される場所に注目して下さい。カレント位置が1つ進み、レコード数が1つ増えます。

挿入位置が先頭あるいは最後尾のときで振る舞いが異なるため、QueryPos()で位置を確認し、Add()メソッドとInsert()メソッドを使い分けます。以下に、指定された位置にイベントを挿入するコード例を示します。

Error CStream::Insert(
    int         pos,
    CForm*      form)
{
    switch (QueryPos(pos))
    {
    case NoErr:
        if (pos+1 == NumForm())     m_FormList.Add(*form);
        else                        m_FormList.InsertAt(pos+1, *form);
        break;
    case TopEnd:
        m_FormList.InsertAt(0, *form);
        break;
    case BottomEnd:
        m_FormList.Add(*form);
        break;
    }
    return NoErr;
}
        public Error Insert(
            int         pos,
            Form        form)
        {
            switch (QueryPos(pos))
            {
            case Error.NoErr:
                if (pos+1 == NumForm)       FormList.Add(form);
                else                        FormList.Insert(pos+1, form);
                break;
            case Error.TopEnd:
                FormList.Insert(0, form);
                break;
            case Error.BottomEnd:
                FormList.Add(form);
                break;
            }
            return Error.NoErr;
        }

削除

カレント位置のイベントを削除します。挿入の反対の操作になります。カレント位置が1つ戻り、イベント数が1つ減ります。

以下に、指定された位置のイベントを削除するコード例を示します。

Error CStream::RemoveAt(int pos)
{
    Error err = QueryPos(pos);

    if (err == NoErr)
    {
        m_FormList.RemoveAt(pos);
    }
    return err;
}
        public Error RemoveAt(int pos)
        {
            Error err = QueryPos(pos);

            if (err == Error.NoErr)
            {
                FormList.RemoveAt(pos);
            }
            return err;
        }

この他にも、Selectフラグで選択されたイベントを削除するコマンドや、二つのストリームをマージするコマンドなど、基本コマンドを組み合わせることで、より高機能なコマンドを実装することができます。

追加

SMFを順次読み込む場合など、最後尾に追加すれば良いことが分かっている時は、このメソッドを使います。範囲チェックが省略されているため、高速で動作します。

Error CStream::Add(CForm* form)
{
    m_FormList.Add(*form);
    return NoErr;
}
        public Error Add(Form form)
        {
            FormList.Add(form);
            return Error.NoErr;
        }

初期化

以下のメソッドは、ストリームを初期化します。

void CStream::Initialize()
{
    m_FormList.RemoveAll();
    m_FormList.FreeExtra();
    SeekTop();
}
        public void Initialize()
        {
            FormList.Clear();
            FormList.TrimExcess();
            SeekTop();
        }

イベントの読み出しに関連する機能

ストリームには、直前に読み出したイベントの位置、あるいは位置の移動コマンドによって設定されたイベントの位置が記録されています。この位置を基準に、前方あるいは後方のイベントを読み出すことができます。これらのメソッドは、forなどでループする時に用います。また指定した位置のイベントを直接読み出すこともできます。

CForm* CStream::ReadNext()
{
    return ReadPos(m_Position + 1);
}

CForm* CStream::ReadPrev()
{
    return ReadPos(m_Position - 1);
}

CForm* CStream::ReadCur()
{
    return ReadPos(m_Position);
}

CForm* CStream::ReadPos(int pos)
{
    Error err = SeekPos(pos);

    if (err == NoErr)
        return &m_FormList[pos];
    else
        return NULL;
}
        public Form ReadNext()
        {
            return ReadPos(Position + 1);
        }

        public Form ReadPrev()
        {
            return ReadPos(Position - 1);
        }

        public Form ReadCur()
        {
            return ReadPos(Position);
        }

        public Form ReadPos(int pos)
        {
            Error err = SeekPos(pos);

            if (err == Error.NoErr)
                return FormList[pos];
            else
                return null;
        }

エラーコード

エラーコードは、Streamクラス専用で定義しても良いですし、システム全体で共通のものを定義することも可能です。

enum Error
{
    NoErr,
    TopEnd,
    BottomEnd,
};
    public enum Error
    {
        NoErr,
        TopEnd,
        BottomEnd,
    }

ドキュメントの先頭へ

カクタスソフトウェア 技術協力 資料室 資料室の広場 SourceForge.jp お問い合わせ