2017年5月4日木曜日

C#でMIDI その6

 C#でMIDIの続きです。宣言通り、もう少し美しい形に修正しました。

 まず、MIDIファイル (SMF0, SMF1) に対応させました。それに合わせて、データを複数トラックで保持する形にしました。
 また、時間の単位をtickにしました。理由はmsec単位に変換してからtickに戻すと、計算誤差が出るからです。

 これまで、Sleep関数を使っていましたが、それだと時間の誤差が発生したため、Sleep関数をやめました(youtubeを見ながら、プログラムを実行したら、動作が遅くなったので。。。)。

 ユーザーインターフェースも変わっています。これも、さんざん悩んだ結果です。機能を増やすと、バグの元になるので、ボタン1つしかありません。
 ファイルの読み込みはプログラムの起動時のみです。そのまま実行すると、midi.txtを読み込みます。ドラッグ&ドロップとかで、midiファイルを引数にすると、midiファイルを読み込めます。Loadのところをよく読めば分かります。(ちょっと分かりにくい。。。)

 1000行近いプログラムになってしまいました。分かりにくいので、主要な関数を説明しておきます。

  • play
    • Midiの再生。Midi Eventを並べ替えて、時間順にする。
  • SplitMidiMesaage
    • TextファイルとSMF0ファイルにはトラックがないので、Midi EventをChannel毎にトラックに分割する。
  • MergeMidiMessage()
    • Midiの再生、SMF0での保存のため、トラックに分割されたMidi Eventを1つにまとめる。
  • SaveText
    • Textファイルで保存する
  • LoadText
    • Textファイルを読み込む
  • SaveSmf
    • SMF0, SMF1でMidiを保存する
  • LoadSmf
    • SMF0, SMF1でMidiを読み込む
  • MsgToData
    • Midiを保存するため、Midi Eventをbyte配列に変換する
  • DataToMsg
    • Midiを読み込むため、byte配列をMidi Eventに変換する

 例によって、テストが甘いので、バグがあるかもしれません。ご勘弁。
 その代わり、ソースコードの改造はご自由にどうぞ。

--------------------------------------------------
2017/05/04
APIの宣言に間違いを見つけました。
ハンドル(hmo)をuint(4byte)で宣言していますが、これが正しいのは32bit OSです。64bit OSの場合は、long(8byte)とかにしないとダメです。
(コンパイルできるし、プログラムも動いちゃうんですが。。。)

下記のソースコードは修正していないので、これを利用しようなんて考えている変わり者の方がいましたら、ご注意ください。

--------------------------------------------------


using System;
using System.IO;
using System.Threading;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Windows.Forms;

class Program
{
[STAThread]

static void Main()
{
Application.Run( new FormMidi());
}
}

class FormMidi : Form
{
/*--------------------------------------------------*/
/* API */
/*--------------------------------------------------*/
[DllImport( "Winmm.dll")]
extern static uint midiOutGetNumDevs();

[DllImport( "Winmm.dll")]
extern static uint midiOutOpen( ref uint lphmo, uint uDeviceID, uint dwCallback, uint dwCallbackInstance, uint dwFlags);

[DllImport( "Winmm.dll")]
extern static uint midiOutClose( uint hmo);

[DllImport( "Winmm.dll")]
extern static uint midiOutShortMsg( uint hmo, uint dwMsg);

[DllImport( "Winmm.dll")]
extern static uint midiOutReset( uint hmo);

private const uint MMSYSERR_NOERROR = 0;

private const uint MMSYSERR_BADDEVICEID = 2;
private const uint MMSYSERR_ALLOCATED = 4;
private const uint MMSYSERR_NOMEM = 7;
private const uint MMSYSERR_INVALPARAM = 11;
private const uint MMSYSERR_NODEVICE = 68;

private const uint MMSYSERR_INVALHANDLE = 5;
private const uint MIDIERR_STILLPLAYING = 65;

private const uint MIDI_MAPPER = 0xffffffff;

/*--------------------------------------------------*/
/* MIDI Message Class */
/*--------------------------------------------------*/
private class MidiMessage
{
public int Time; // tick (it's not relative time)
public byte Event; // 0x80 - 0xef
public byte Param1; // 0x00 - 0x7f
public byte Param2; // 0x00 - 0x7f

public MidiMessage( int tm, byte evt, byte param1, byte param2)
{
this.Time = tm;
this.Event = evt;
this.Param1 = param1;
this.Param2 = param2;
}
}

/*--------------------------------------------------*/
/* Variables */
/*--------------------------------------------------*/
private uint hMidi;

private TextBox tb;
private Button btn;

private string text;

private Thread thread;

private MidiMessage[][] msg; //Midi Event Message
private int div; //Midi Time Division [tick]

/*--------------------------------------------------*/
/* FormMidi */
/*--------------------------------------------------*/
public FormMidi()
{
this.Text = "Midi";
this.ClientSize = new Size( 360, 180);

this.Load += new EventHandler( this.FormMidi_Load);
this.Closed += new EventHandler( this.FormMidi_Closed);

this.tb = new TextBox();
this.tb.SetBounds( 8, 8, 344, 116);
this.tb.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right;
this.tb.ReadOnly = true;
this.tb.Multiline = true;
this.tb.BorderStyle = BorderStyle.None;
    this.tb.Font = new Font( "Arial", 10);
this.Controls.Add( this.tb);

this.btn = new Button();
this.btn.SetBounds( 8, 128, 120, 40);
this.btn.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
    this.btn.Font = new Font( "Arial", 10);
this.btn.Text = "Play";
this.btn.Click += new EventHandler( this.btn_Click);
this.Controls.Add( this.btn);
}

/*--------------------------------------------------*/
/* Load */
/*--------------------------------------------------*/
private void FormMidi_Load( object sender, EventArgs e)
{
Console.WriteLine( "FormMidi_Load");

/*--------------------------------------------------*/
Console.WriteLine( "MIDI Open");

uint ret = midiOutGetNumDevs();
Console.WriteLine( "Number of Midi Device : " + ret);

if( midiOutOpen( ref this.hMidi, MIDI_MAPPER, 0, 0, 0) != MMSYSERR_NOERROR)
{
MessageBox.Show( "midiOutOpen error");
Application.Exit();
}

/*--------------------------------------------------*/
string path = Application.ExecutablePath;
path = Path.GetDirectoryName( path);
Console.WriteLine( "work path:\t" + path);
string filename = path + @"\midi.txt";

Console.WriteLine( filename);

if( File.Exists( filename) == false)
{
this.SampleMidi();
}
else
{
this.LoadText( filename);
}

/*--------------------------------------------------*/
string[] args = Environment.GetCommandLineArgs();

if( 1 < args.Length)
{
if( args[1].Substring( args[1].Length - 3) == "mid")
{
filename = args[1];
this.LoadSmf( filename);
}
}

/*--------------------------------------------------*/
this.SaveText( path + @"\midi.txt");
this.SaveSmf( path + @"\smf0.mid", 0);
this.SaveSmf( path + @"\smf1.mid", 1);

/*--------------------------------------------------*/
this.text = "File Name:\t" + Path.GetFileName( filename) + "\r\n";

int count = 0;
for( int i = 0; i < this.msg.Length; i++)
{
count += this.msg[i].Length;
}

this.text += "Midi Event:\t" + count + "\r\n";
this.text += "Time Division:\t" + this.div + "\r\n";
this.text += "\r\n";
this.text += "No." + "\t" + "time" + "\t" + "event" + "\t" + "param1" + "\t" + "param2" + "\t" + "\r\n";

this.tb.Text = this.text;
this.tb.Select( 0, 0);

/*--------------------------------------------------*/
}

/*--------------------------------------------------*/
/* Closed */
/*--------------------------------------------------*/
private void FormMidi_Closed( object sender, EventArgs e)
{
Console.WriteLine( "FormMidi_Closed");

midiOutReset( this.hMidi);

Console.WriteLine( "MIDI Close");
midiOutClose( this.hMidi);
}

/*--------------------------------------------------*/
/* Clicked */
/*--------------------------------------------------*/
private void btn_Click( object sender, EventArgs e)
{
Console.WriteLine( "btn_Click");

if( this.thread == null || this.thread.IsAlive == false)
{
this.btn.Text = "Stop";

ThreadStart ts = new ThreadStart( this.play);
this.thread = new Thread( ts);
this.thread.Priority = ThreadPriority.Normal;
this.thread.IsBackground = true;
this.thread.Start();
}
else
{
this.btn.Text = "Play";

midiOutReset( this.hMidi);
this.thread.Abort();
}
}

/*--------------------------------------------------*/
/* Play */
/*--------------------------------------------------*/
private void play()
{
Console.WriteLine( "play");

/*--------------------------------------------------*/
MidiMessage[] mm = this.MergeMidiMessage( this.msg);

/*--------------------------------------------------*/
long t0 = DateTime.Now.Ticks / 10000; // [msec]
long t1 = 0; // [msec]
long t2 = 0; // [msec]

for( int i = 0; i < mm.Length; i++)
{
/*--------------------------------------------------*/
while( t1 * this.div / 500 < mm[i].Time)
{
t1 = DateTime.Now.Ticks / 10000 - t0;
}

midiOutShortMsg( this.hMidi, (uint) ( ( mm[i].Param2 << 16) + ( mm[i].Param1 << 8) + mm[i].Event));

/*--------------------------------------------------*/
string str = i.ToString() + "\t";
str += mm[i].Time.ToString() + "\t";
str += mm[i].Event.ToString( "X") + "\t";
str += mm[i].Param1.ToString() + "\t";
str += mm[i].Param2.ToString() + "\t";

Console.WriteLine( str);

if( t2 < t1)
{
this.tb.Text= this.text + str; // <- it cause a little delay
t2 += 10; // update textbox every 10[msec]
}

/*--------------------------------------------------*/
}

/*--------------------------------------------------*/
this.btn.Text = "Play";

midiOutReset( this.hMidi);
}

/*--------------------------------------------------*/
/* Sort */
/*--------------------------------------------------*/
private void SortTime( MidiMessage[] mm)
{
MidiMessage temp;

for( int i = 1; i < mm.Length; i++)
{
temp = mm[i];

int j;

for( j = i; 0 < j; j--)
{
if( temp.Time < mm[j - 1].Time)
{
mm[j] = mm[j - 1];
}
else
{
break;
}
}

mm[j] = temp;
}
}

/*--------------------------------------------------*/
/* SampleMidi */
/*--------------------------------------------------*/
private void SampleMidi()
{
this.div = 96;

this.msg = new MidiMessage[3][];
this.msg[0] = new MidiMessage[0];
this.msg[1] = new MidiMessage[17];
this.msg[2] = new MidiMessage[32];

byte[] key = new byte[8];
key[0] = 60;
key[1] = 62;
key[2] = 64;
key[3] = 65;
key[4] = 67;
key[5] = 69;
key[6] = 71;
key[7] = 72;

this.msg[1][0] = new MidiMessage( 0, 0xc0, 60, 0);

for( int i = 0; i < 8; i++)
{
this.msg[1][2 * i + 1] = new MidiMessage( 96 * i, 0x90, key[i], 127);
this.msg[1][2 * i + 2] = new MidiMessage( 96 * ( i + 1), 0x80, key[i], 127);
}

for( int i = 0; i < 16; i++)
{
this.msg[2][2 * i + 0] = new MidiMessage( 48 * i, 0x99, 36, 127);
this.msg[2][2 * i + 1] = new MidiMessage( 48 * ( i + 1), 0x89, 36, 127);
}
}

/*--------------------------------------------------*/
/* SplitMidiMessage */
/* textファイル、smf0のファイルを読み込んだ場合に、channel毎に分割する */
/*--------------------------------------------------*/
private MidiMessage[][] SplitMidiMessage( MidiMessage[] mm)
{
/*--------------------------------------------------*/
int[] n1 = new int[16]; // 各channelのmidi eventの数
int[] n2 = new int[16]; // 各channelを保存する配列のindex

for( int i = 0; i < n1.Length; i++)
{
n1[i] = 0;
}

int ch;

for( int i = 0; i < mm.Length; i++)
{
ch = mm[i].Event & 0x0f;
n1[ch]++;
}

int count = 1;

for( int i = 0; i < n1.Length; i++)
{
if( 0 < n1[i])
{
n2[i] = count;
count++;
}
}

/*--------------------------------------------------*/
MidiMessage[][] mmm = new MidiMessage[count][];

mmm[0] = new MidiMessage[0]; // Conductor Track

count = 1;

for( int i = 0; i < n1.Length; i++)
{
if( 0 < n1[i])
{
mmm[count] = new MidiMessage[n1[i]];
count++;
}

n1[i] = 0;
}

for( int i = 0; i < mm.Length; i++)
{
ch = mm[i].Event & 0x0f;
mmm[n2[ch]][n1[ch]] = mm[i];
n1[ch]++;
}

/*--------------------------------------------------*/
for( int i = 0; i < mmm.Length; i++)
{
this.SortTime( mmm[i]);
}

/*--------------------------------------------------*/

return mmm;
}

/*--------------------------------------------------*/
/* MergeMidiMessage */
/*--------------------------------------------------*/
private MidiMessage[] MergeMidiMessage( MidiMessage[][] mmm)
{
int count = 0;

for( int i = 0; i < mmm.Length; i++)
{
count += mmm[i].Length;
}

MidiMessage[] mm = new MidiMessage[count];
count = 0;

for( int i = 0; i < mmm.Length; i++)
{
for( int j = 0; j < mmm[i].Length; j++)
{
mm[count] = mmm[i][j];
count++;
}
}

this.SortTime( mm);

return mm;
}

/*--------------------------------------------------*/
/* SaveText */
/*--------------------------------------------------*/
private void SaveText( string filename)
{
Console.WriteLine( "SaveText");

try
{
StreamWriter sw = new StreamWriter( filename, false); // The file is overwritten

/*--------------------------------------------------*/
sw.WriteLine( "MidiText");
sw.WriteLine( "TimeDivision"  + "\t" + this.div);
sw.WriteLine();
sw.WriteLine( "\t" + "time" + "\t" + "event" + "\t" + "channel" + "\t" + "param1" + "\t" + "param2");
sw.WriteLine( "\t" + "[tick]" + "\t" + "0x8-0xf" + "\t" + "0-15" + "\t" + "0-127" + "\t" + "0-127");
sw.WriteLine();

/*--------------------------------------------------*/
int tm;
byte evt;
byte ch;
byte param1;
byte param2;

for( int i = 0; i < this.msg.Length; i++)
{
sw.WriteLine( "Track " + i);

for( int j = 0; j < this.msg[i].Length; j++)
{
tm = (int) this.msg[i][j].Time;
evt = (byte) ( (this.msg[i][j].Event & 0xf0) >> 4);
ch = (byte) (this.msg[i][j].Event & 0x0f);
param1 = (byte) (this.msg[i][j].Param1);
param2 = (byte) (this.msg[i][j].Param2);

sw.WriteLine( ":" + "\t" + tm + "\t0x" + evt.ToString( "X") + "\t" + ch + "\t" + param1 + "\t" + param2);
}

sw.WriteLine();
}

sw.Close();

/*--------------------------------------------------*/
}
catch( Exception e)
{
MessageBox.Show( e.Message);
}
}

/*--------------------------------------------------*/
/* LoadText */
/*--------------------------------------------------*/
private void LoadText( string filename)
{
Console.WriteLine( "LoadText");

try
{
/*--------------------------------------------------*/
StreamReader sr = new StreamReader( filename);

string txt = sr.ReadToEnd();
txt.Replace( "\r\n", "\n");
string[] lines = txt.Split( '\n');
string[] str;

sr.Close();

/*--------------------------------------------------*/
str = lines[1].Split( '\t');
this.div = Convert.ToInt32( str[1]);

/*--------------------------------------------------*/
int[] n = new int[lines.Length]; // Midi Messageが書いてある行の位置
int count = 0; // Midi Messageが書いてある行の行数

for( int i = 0; i < lines.Length; i++)
{
if( 0 < lines[i].Length && lines[i].Substring( 0, 1) == ":")
{
n[count] = i;
count++;
}
}

/*--------------------------------------------------*/
MidiMessage[] mm = new MidiMessage[count];

int tm;
byte evt;
byte ch;
byte param1;
byte param2;

for( int i = 0; i < mm.Length; i++)
{
str = lines[n[i]].Split( '\t');

tm = (int) Convert.ToInt32( str[1]);
evt = (byte) Convert.ToInt32( str[2], 16);
ch = (byte) Convert.ToInt32( str[3]);
param1 = (byte) Convert.ToInt32( str[4]);
param2 = (byte) Convert.ToInt32( str[5]);

mm[i] = new MidiMessage( tm, (byte)( ( evt << 4) + ch), param1, param2);
}

/*--------------------------------------------------*/
this.msg = this.SplitMidiMessage( mm);

/*--------------------------------------------------*/
}
catch( Exception e)
{
MessageBox.Show( e.Message);
}
}

/*--------------------------------------------------*/
/* SaveSmf */
/*--------------------------------------------------*/
private void SaveSmf( string filename, int FormatType)
{
Console.WriteLine( "SaveSmf");

try
{
FileStream ofs = new FileStream( filename, FileMode.Create, FileAccess.Write);

/*--------------------------------------------------*/
/* Header Chunk */
/*--------------------------------------------------*/
if( FormatType == 0)
{
byte[] hc = this.HeaderChunk( 0, 1, this.div);
ofs.Write( hc, 0, hc.Length);
}
else if( FormatType == 1)
{
byte[] hc = this.HeaderChunk( 1, this.msg.Length, this.div);
ofs.Write( hc, 0, hc.Length);
}

/*--------------------------------------------------*/
/* Track Chunk */
/*--------------------------------------------------*/
if( FormatType == 0)
{
MidiMessage[] mm = this.MergeMidiMessage( this.msg);
byte[] data = this.MsgToData( mm);
byte[] tc = this.TrackChunk( data.Length);

ofs.Write( tc, 0, tc.Length);
ofs.Write( data, 0, data.Length);
}
else if( FormatType == 1)
{
for( int i = 0; i < this.msg.Length; i++)
{
byte[] data = this.MsgToData( this.msg[i]);
byte[] tc = this.TrackChunk( data.Length);

ofs.Write( tc, 0, tc.Length);
ofs.Write( data, 0, data.Length);
}
}

/*--------------------------------------------------*/
ofs.Close();
}
catch( Exception e)
{
MessageBox.Show( e.Message);
}
}

/*--------------------------------------------------*/
/* HeaderChunk */
/*--------------------------------------------------*/
private byte[] HeaderChunk( int FormatType, int NumberOfTracks, int TimeDivision)
{
byte[] hc = new byte[14];

hc[0] = (byte) 'M';
hc[1] = (byte) 'T';
hc[2] = (byte) 'h';
hc[3] = (byte) 'd';
hc[4] = (byte) 0;
hc[5] = (byte) 0;
hc[6] = (byte) 0;
hc[7] = (byte) 6;
hc[8] = (byte) 0;
hc[9] = (byte) FormatType;
hc[10] = (byte) ( ( NumberOfTracks >> 8) & 255);
hc[11] = (byte) ( NumberOfTracks & 255);
hc[12] = (byte) ((  TimeDivision >> 8) & 255);
hc[13] = (byte) ( TimeDivision & 255);

return hc;
}

/*--------------------------------------------------*/
/* TrackChunk */
/*--------------------------------------------------*/
private byte[] TrackChunk( int ChunkSize)
{
byte[] tc = new byte[8];

tc[0] = (byte) 'M';
tc[1] = (byte) 'T';
tc[2] = (byte) 'r';
tc[3] = (byte) 'k';

tc[4] = (byte) ( ( ChunkSize >> 24) & 255);
tc[5] = (byte) ( ( ChunkSize >> 16) & 255);
tc[6] = (byte) ( ( ChunkSize >> 8) & 255);
tc[7] = (byte) ( ChunkSize & 255);

return tc;
}

/*--------------------------------------------------*/
/* LoadSmf */
/*--------------------------------------------------*/
private void LoadSmf( string filename)
{
Console.WriteLine( "LoadSmf");

try
{
/*--------------------------------------------------*/
FileStream ifs = new FileStream( filename, FileMode.Open, FileAccess.Read);

/*--------------------------------------------------*/
/* Header Chunk */
/*--------------------------------------------------*/
byte[] hc = new byte[14];
ifs.Read( hc, 0, hc.Length);

int FormatType = ( hc[8] << 8) + hc[9];
int NumberOfTracks = ( hc[10] << 8) + hc[11];
this.div = ( hc[12] << 8) + hc[13];

/*--------------------------------------------------*/
/* Track Chunk */
/*--------------------------------------------------*/
if( FormatType == 0)
{
byte[] tc = new byte[8];
ifs.Read( tc, 0, tc.Length);

int ChunkSize = ( tc[4] << 24) + ( tc[5] << 16) + (tc[6] << 8) + tc[7];
byte[] data = new byte[ChunkSize];
ifs.Read( data, 0, data.Length);

MidiMessage[] mm = this.DataToMsg( data);
this.msg = this.SplitMidiMessage( mm);
}
else if( FormatType == 1)
{
this.msg = new MidiMessage[NumberOfTracks][];

for( int i = 0; i < this.msg.Length; i++)
{
byte[] tc = new byte[8];
ifs.Read( tc, 0, tc.Length);

int ChunkSize = ( tc[4] << 24) + ( tc[5] << 16) + (tc[6] << 8) + tc[7];
byte[] data = new byte[ChunkSize];
ifs.Read( data, 0, data.Length);

this.msg[i] = this.DataToMsg( data);
}
}

ifs.Close();

/*--------------------------------------------------*/
}
catch( Exception e)
{
MessageBox.Show( e.Message);
}
}

/*--------------------------------------------------*/
/* MsgToData */
/*--------------------------------------------------*/
byte[] MsgToData( MidiMessage[] mm)
{
byte[] buf = new byte[mm.Length * 8 + 4];

int d_pos = 0;
int m_pos = 0;

/*--------------------------------------------------*/
while( m_pos < mm.Length)
{
/*--------------------------------------------------*/
int dt = mm[m_pos].Time;
dt -= ( 0 < m_pos ? mm[m_pos - 1].Time : 0);

int temp = 7;

while( ( 1 << temp) < dt)
{
temp += 7;
}

while( 7 < temp)
{
temp -= 7;
buf[d_pos] = (byte)( ( ( dt >> temp) & 127) + 128);
d_pos++;
}

buf[d_pos] = (byte)( dt & 127);
d_pos++;

/*--------------------------------------------------*/
if( m_pos == 0 || 0xf0 <= mm[m_pos].Event || mm[m_pos].Event != mm[m_pos - 1].Event)
{
buf[d_pos] = mm[m_pos].Event;
d_pos++;
}

/*--------------------------------------------------*/
if( mm[m_pos].Event < 0xf0)
{
buf[d_pos] = mm[m_pos].Param1;
d_pos++;

if( mm[m_pos].Event < 0xc0 || 0xe0 <= mm[m_pos].Event)
{
buf[d_pos] = mm[m_pos].Param2;
d_pos++;
}
}

/*--------------------------------------------------*/
m_pos++;
}

/*--------------------------------------------------*/
buf[d_pos + 0] = (byte) 0;
buf[d_pos + 1] = (byte) 0xff;
buf[d_pos + 2] = (byte) 0x2f;
buf[d_pos + 3] = (byte) 0;
d_pos += 4;

/*--------------------------------------------------*/
byte[] ret = new byte[d_pos];

for( int i = 0; i < ret.Length; i++)
{
ret[i] = buf[i];
}

return ret;
}

/*--------------------------------------------------*/
/* DataToMsg */
/*--------------------------------------------------*/
private MidiMessage[] DataToMsg( byte[] data)
{
MidiMessage[] buf = new MidiMessage[data.Length / 2];

int m_pos = 0;
int d_pos = 0;

int tm = 0;
byte evt = 0;
byte param1 = 0;
byte param2 = 0;

/*--------------------------------------------------*/
while( d_pos < data.Length)
{
/*--------------------------------------------------*/
tm = data[d_pos] & 127;
d_pos++;

while( 0x80 <= data[d_pos - 1])
{
tm = ( tm << 7) + ( data[d_pos] & 127);
d_pos++;
}

/*--------------------------------------------------*/
if( 0x80 <= data[d_pos])
{
evt = data[d_pos];
d_pos++;
}

/*--------------------------------------------------*/
if( evt < 0xf0)
{
param1 = data[d_pos];
d_pos++;

if( evt < 0xc0 || 0xe0 <= evt)
{
param2 = data[d_pos];
d_pos++;
}
else
{
param2 = 0;
}
}
else
{
// System Event or Meta Event

if( evt == 0xff)
{
param1 = data[d_pos];
d_pos++;
}

int temp = data[d_pos] & 127;
d_pos++;

while( 0x80 <= data[d_pos - 1])
{
temp = ( temp << 7) + ( data[d_pos] & 127);
d_pos++;
}

d_pos += temp;
}

/*--------------------------------------------------*/
if( evt < 0xf0)
{
buf[m_pos] = new MidiMessage( tm, evt, param1, param2);
m_pos++;
}
}

/*--------------------------------------------------*/
for( int i = 1; i < m_pos; i++)
{
buf[i].Time += buf[i - 1].Time;
}

/*--------------------------------------------------*/
MidiMessage[] ret = new MidiMessage[m_pos];

for( int i = 0; i < ret.Length; i++)
{
ret[i] = buf[i];
}

return ret;
}
}

C#でMIDI その5

 C#でMIDIの続きです。

 今回はMIDI Playerを作ります。前回までで、MIDIファイルを読み込めて、MIDIで音を出せるのだから、後はその組み合わせです。・・・でも、簡単ではないです。

 MIDIファイルにはMIDIイベントが時系列に並んでいるわけではありません。なので、MIDIイベントの順序を入れ替えるSortが必要です。
 また実際作ってみると、MIDIイベント間の時間待ちにSleep関数を使うと、演奏にずれが生じることが分かりました。というわけで、Sleep関数を使わない方式に変えました。

 ソースコードはご自由にご利用ください。ただし、バグがあっても知りません。
 趣味のプログラムということで、ご了承ください。



using System;
using System.IO;
using System.Threading;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Windows.Forms;

class Program
{
[STAThread]

static void Main()
{
Application.Run( new FormMidi());
}
}

/*--------------------------------------------------*/
/* FormMidi Class */
/*--------------------------------------------------*/
class FormMidi : Form
{
private Label lbl;
private Button btn_open;
private Button btn_play;
private Button btn_stop;

private MidiPlayer Player;

/*--------------------------------------------------*/
/* FormMidi */
/*--------------------------------------------------*/
public FormMidi()
{
this.Text = "Midi";
this.ClientSize = new Size( 240, 120);
this.FormBorderStyle = FormBorderStyle.FixedSingle;

this.Load += new EventHandler( this.FormMidi_Load);
this.Closed += new EventHandler( this.FormMidi_Closed);

/*--------------------------------------------------*/
this.AllowDrop = true;
this.DragEnter += new DragEventHandler( this.FormMidi_DragEnter);
this.DragDrop += new DragEventHandler( this.FormMidi_DragDrop);

/*--------------------------------------------------*/
this.lbl = new Label();
this.lbl.SetBounds( 20, 10, 200, 60);
    this.lbl.Font = new Font( "Arial", 10);
this.lbl.Text = "File : " + "\r\n";
this.lbl.Text += "Track : " + "\r\n";
this.lbl.Text += "Event : " + "\r\n";
this.lbl.Text += "TimeDivision : " + "\r\n";
//this.lbl.BorderStyle = BorderStyle.FixedSingle;
this.Controls.Add( this.lbl);

/*--------------------------------------------------*/
this.btn_open = new Button();
this.btn_open.SetBounds( 15, 80, 70, 30);
    this.btn_open.Font = new Font( "Arial", 10);
this.btn_open.Text = "Open";
this.btn_open.Click += new EventHandler( this.btn_open_Click);
this.Controls.Add( this.btn_open);

/*--------------------------------------------------*/
this.btn_play = new Button();
this.btn_play.SetBounds( 85, 80, 70, 30);
    this.btn_play.Font = new Font( "Arial", 10);
this.btn_play.Text = "Play";
this.btn_play.Click += new EventHandler( this.btn_play_Click);
this.Controls.Add( this.btn_play);

/*--------------------------------------------------*/
this.btn_stop = new Button();
this.btn_stop.SetBounds( 155, 80, 70, 30);
    this.btn_stop.Font = new Font( "Arial", 10);
this.btn_stop.Text = "Stop";
this.btn_stop.Click += new EventHandler( this.btn_stop_Click);
this.Controls.Add( this.btn_stop);
}

/*--------------------------------------------------*/
/* FormMidi_Load */
/*--------------------------------------------------*/
private void FormMidi_Load( object sender, EventArgs e)
{
this.Player = new MidiPlayer();

/*--------------------------------------------------*/
string[] args = Environment.GetCommandLineArgs();

if( 1 < args.Length)
{
this.OpenFile( args[1]);
}
}

/*--------------------------------------------------*/
/* FormMidi_Closed */
/*--------------------------------------------------*/
private void FormMidi_Closed( object sender, EventArgs e)
{
if( this.Player.IsPlaying() == true)
{
this.Player.PlayStop();
}
}

/*--------------------------------------------------*/
/* DragEnter */
/*--------------------------------------------------*/
private void FormMidi_DragEnter( object sender, DragEventArgs e)
{
if( e.Data.GetDataPresent( DataFormats.FileDrop))
{
e.Effect = DragDropEffects.Move;
}
}

/*--------------------------------------------------*/
/* DragDrop */
/*--------------------------------------------------*/
private void FormMidi_DragDrop( object sender, DragEventArgs e)
{
string[] filename = (string[]) e.Data.GetData( DataFormats.FileDrop, false);
this.OpenFile( filename[0]);
}

/*--------------------------------------------------*/
/* btn_open_Click */
/*--------------------------------------------------*/
private void btn_open_Click( object sender, EventArgs e)
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "midi file|*.mid|*|*.*";

if( ofd.ShowDialog() == DialogResult.OK)
{
this.OpenFile( ofd.FileName);
}
}

/*--------------------------------------------------*/
/* btn_play_Click */
/*--------------------------------------------------*/
private void btn_play_Click( object sender, EventArgs e)
{
if( this.Player.IsPlaying() == true)
{
this.Player.PlayStop();
}

this.Player.PlayStart();
}

/*--------------------------------------------------*/
/* btn_stop_Click */
/*--------------------------------------------------*/
private void btn_stop_Click( object sender, EventArgs e)
{
if( this.Player.IsPlaying() == true)
{
this.Player.PlayStop();
}
}

/*--------------------------------------------------*/
/* OpenFile */
/*--------------------------------------------------*/
private void OpenFile( string filename)
{
if( this.Player.IsPlaying() == true)
{
this.Player.PlayStop();
}

if( Path.GetExtension( filename) == ".mid")
{
this.Player.Load( filename);

this.lbl.Text = "File : " + Path.GetFileName( filename) + "\r\n";
this.lbl.Text += "Track : " + this.Player.Track.Length + "\r\n";
this.lbl.Text += "Event : " + this.Player.GetEventCount() + "\r\n";
this.lbl.Text += "TimeDivision : " + this.Player.TimeDivision + "\r\n";
}

this.Player.PlayStart();
}
}



/*--------------------------------------------------*/
/* MidiEvent Class */
/*--------------------------------------------------*/
public class MidiEvent
{
public uint Time; // DeltaTime [tick]
public byte Event; // 0x80 - 0xff
}

/*--------------------------------------------------*/
/* ChannelEvent Class */
/*--------------------------------------------------*/
public class ChannelEvent : MidiEvent
{
public byte Param1; // 0x00 - 0x7f
public byte Param2; // 0x00 - 0x7f

public ChannelEvent( uint tm, byte evt, byte p1, byte p2)
{
this.Time = tm;
this.Event = evt;
this.Param1 = p1;
this.Param2 = p2;
}
}

/*--------------------------------------------------*/
/* SystemEvent Class */
/*--------------------------------------------------*/
public class SystemEvent : MidiEvent
{
public byte[] Data;

public SystemEvent( uint tm, byte evt, byte[] dt)
{
this.Time = tm;
this.Event = evt;
this.Data = dt;
}
}

/*--------------------------------------------------*/
/* MetaEvent Class */
/*--------------------------------------------------*/
public class MetaEvent : MidiEvent
{
public byte Type;
public byte[] Data;

public MetaEvent( uint tm, byte evt, byte tp, byte[] dt)
{
this.Time = tm;
this.Event = evt;
this.Type = tp;
this.Data = dt;
}
}



/*--------------------------------------------------*/
/* MidiPlayer Class */
/*--------------------------------------------------*/
public class MidiPlayer
{
/*--------------------------------------------------*/
/* API */
/*--------------------------------------------------*/
[DllImport( "Winmm.dll")]
extern static uint midiOutGetNumDevs();

[DllImport( "Winmm.dll")]
extern static uint midiOutOpen( ref long lphmo, uint uDeviceID, uint dwCallback, uint dwCallbackInstance, uint dwFlags);

[DllImport( "Winmm.dll")]
extern static uint midiOutClose( long hmo);

[DllImport( "Winmm.dll")]
extern static uint midiOutShortMsg( long hmo, uint dwMsg);

[DllImport( "Winmm.dll")]
extern static uint midiOutReset( long hmo);

private const uint MMSYSERR_NOERROR = 0;

private const uint MMSYSERR_BADDEVICEID = 2;
private const uint MMSYSERR_ALLOCATED = 4;
private const uint MMSYSERR_NOMEM = 7;
private const uint MMSYSERR_INVALPARAM = 11;
private const uint MMSYSERR_NODEVICE = 68;

private const uint MMSYSERR_INVALHANDLE = 5;
private const uint MIDIERR_STILLPLAYING = 65;

private const uint MIDI_MAPPER = 0xffffffff;

/*--------------------------------------------------*/
/* Variables */
/*--------------------------------------------------*/
private long hMidi;
private Thread PlayThread;

public uint TimeDivision;
public MidiEventArray[] Track;

/*--------------------------------------------------*/
/* MidiPlayer */
/*--------------------------------------------------*/
public MidiPlayer()
{
this.hMidi = 0;
ThreadStart ts = new ThreadStart( this.Play);
this.PlayThread = new Thread( ts);
this.Track = new MidiEventArray[0];
this.TimeDivision = 96;
}

/*--------------------------------------------------*/
/* Load */
/*--------------------------------------------------*/
public void Load( string filename)
{
int temp;

FileStream ifs = new FileStream( filename, FileMode.Open, FileAccess.Read);

byte[] hc = new byte[14];
ifs.Read( hc, 0, hc.Length);

temp = ( hc[10] << 8) + hc[11];
this.Track = new MidiEventArray[temp];

temp = ( hc[12] << 8) + hc[13];
this.TimeDivision = (uint) temp;

for( int i = 0; i < this.Track.Length; i++)
{
byte[] tc = new byte[8];
ifs.Read( tc, 0, tc.Length);

temp = ( tc[4] << 24) + ( tc[5] << 16) + (tc[6] << 8) + tc[7];
byte[] data = new byte[temp];
ifs.Read( data, 0, data.Length);

this.Track[i] = new MidiEventArray();
this.Track[i].FromByte( data);
}

ifs.Close();
}

/*--------------------------------------------------*/
/* GetEventCount */
/*--------------------------------------------------*/
public int GetEventCount()
{
int ret = 0;

for( int i = 0; i < this.Track.Length; i++)
{
ret += this.Track[i].Count;
}

return ret;
}

/*--------------------------------------------------*/
/* IsPlaying */
/*--------------------------------------------------*/
public bool IsPlaying()
{
bool ret = true;

if( this.PlayThread.IsAlive == false)
{
ret = false;
}

return ret;
}

/*--------------------------------------------------*/
/* PlayStart */
/*--------------------------------------------------*/
public void PlayStart()
{
if( this.Track.Length == 0)
{
return;
}

if( midiOutOpen( ref this.hMidi, MIDI_MAPPER, 0, 0, 0) != MMSYSERR_NOERROR )
{
MessageBox.Show( "midiOutOpen error");
return;
}

ThreadStart ts = new ThreadStart( this.Play);
this.PlayThread = new Thread( ts);
this.PlayThread.IsBackground = true;
this.PlayThread.Start();
}

/*--------------------------------------------------*/
/* Play */
/*--------------------------------------------------*/
public void Play()
{
MidiEventArray mea = new MidiEventArray();

for( int i = 0; i < this.Track.Length; i++)
{
for( int j = 0; j < this.Track[i].Count; j++)
{
mea.Add( this.Track[i].Event[j]);
}
}

mea.Sort();

/*--------------------------------------------------*/
Console.WriteLine( "----- Play Start -----");

long t0 = DateTime.Now.Ticks / 10000; // [msec]
long t1 = 0; // [msec]
uint td = this.TimeDivision;
uint msg;

for( int i = 0; i < mea.Count; i++)
{
while( t1 * td / 500 < mea.Event[i].Time)
{
t1 = DateTime.Now.Ticks / 10000 - t0;
}

if( mea.Event[i].Event < 0xf0)
{
ChannelEvent ce = (ChannelEvent) mea.Event[i];
msg = (uint) ( ( ce.Param2 << 16) + ( ce.Param1 << 8) + ce.Event);
midiOutShortMsg( this.hMidi, msg);

Console.Write( i.ToString() + "\t");
Console.Write( ce.Time.ToString() + "\t");
Console.Write( ce.Event.ToString( "X") + "\t");
Console.Write( ce.Param1.ToString() + "\t");
Console.Write( ce.Param2.ToString() + "\t");
Console.WriteLine();
}
}

Console.WriteLine( "----- Play Stop -----");

/*--------------------------------------------------*/
midiOutReset( this.hMidi);
midiOutClose( this.hMidi);
}

/*--------------------------------------------------*/
/* PlayStop */
/*--------------------------------------------------*/
public void PlayStop()
{
this.PlayThread.Abort();

midiOutReset( this.hMidi);
midiOutClose( this.hMidi);
}
}

/*--------------------------------------------------*/
/* MidiEventArray Class */
/*--------------------------------------------------*/
public class MidiEventArray
{
public MidiEvent[] Event;
public int Count;

/*--------------------------------------------------*/
/* MidiEventArray */
/*--------------------------------------------------*/
public MidiEventArray()
{
this.Event = new MidiEvent[256];
this.Count = 0;
}

/*--------------------------------------------------*/
/* Add */
/*--------------------------------------------------*/
public void Add( MidiEvent evt)
{
if( this.Count == this.Event.Length - 1)
{
this.MemoryAllocate();
}

this.Event[this.Count] = evt;
this.Count++;
}

/*--------------------------------------------------*/
/* MemoryAllocate */
/*--------------------------------------------------*/
public void MemoryAllocate()
{
MidiEvent[] temp = new MidiEvent[this.Event.Length * 2];

for( int i = 0; i < this.Event.Length; i++)
{
temp[i] = this.Event[i];
}

this.Event = temp;
}

/*--------------------------------------------------*/
/* Sort */
/*--------------------------------------------------*/
public void Sort()
{
MidiEvent temp;

for( int i = 1; i < this.Count; i++)
{
temp = this.Event[i];

int j;

for( j = i; 0 < j; j--)
{
if( temp.Time < this.Event[j - 1].Time)
{
this.Event[j] = this.Event[j - 1];
}
else
{
break;
}
}

this.Event[j] = temp;
}
}

/*--------------------------------------------------*/
/* ReadVariableLength */
/*--------------------------------------------------*/
private static uint ReadVariableLength( byte[] data, ref int pos)
{
uint ret = (uint) ( data[pos] & 0x7f);
pos++;

while( 0x80 <= data[pos - 1])
{
ret = ( ret << 7) + (uint) ( data[pos] & 0x7f);
pos++;
}

return ret;
}

/*--------------------------------------------------*/
/* FromByte */
/*--------------------------------------------------*/
public void FromByte( byte[] data)
{
uint tm = 0;
byte evt = 0;
byte p1 = 0;
byte p2 = 0;
byte tp = 0;
uint len = 0;
byte[] dt;

int pos = 0;

while( pos < data.Length)
{
/*--------------------------------------------------*/
tm += MidiEventArray.ReadVariableLength( data, ref pos);

/*--------------------------------------------------*/
if( 0x80 <= data[pos])
{
evt = data[pos];
pos++;
}

/*--------------------------------------------------*/
if( evt < 0xf0)
{
if( evt < 0xc0 || 0xe0 <= evt)
{
p1 = data[pos];
p2 = data[pos + 1];
pos += 2;
}
else
{
p1 = data[pos];
p2 = 0;
pos += 1;
}

this.Add( new ChannelEvent( tm, evt, p1, p2));
}
else if( evt == 0xf0 || evt == 0xf7)
{
len = MidiEventArray.ReadVariableLength( data, ref pos);
dt = new byte[len];

for( int i = 0; i < dt.Length; i++)
{
dt[i] = data[pos];
pos++;
}

this.Add( new SystemEvent( tm, evt, dt));
}
else if( evt == 0xff)
{
tp = data[pos];
pos++;

len = MidiEventArray.ReadVariableLength( data, ref pos);
dt = new byte[len];

for( int i = 0; i < dt.Length; i++)
{
dt[i] = data[pos];
pos++;
}

this.Add( new MetaEvent( tm, evt, tp, dt));
}

/*--------------------------------------------------*/
}
}
}