2017年12月31日日曜日

小春日和

 今年の冬は、寒い日が続いていますが、たまに晴れた日があると、うれしくなります。

 晴れた日には、布団を干すのが好きです。干している暖かい布団の上で、日向ぼっこをするのが大好きです。

 暖かい日差しの中でだらけていると、いつの間にか日が陰っていたりします。そして、無為に過ごしてしまった時間に少し後悔してしまいます。そうなると知っていて、だらけているんですが。

 そんな小春日和が恋しいです。

科学力

 「科学予算が削減されたせいで、日本の科学力が低下している」なんて話があるそうです。

 この話が正しいかどうかは置いておいて、私が思うに「お金がないから研究ができない」という言い訳はカッコ悪いです。少ない予算でやりくりするのは、誰もが当たり前にやっていることです。お金が必要なら、スポンサーを見つけてお金を集めればいいんです。他にも工夫の余地はいくらでもあります。

 ところで、私見ですが、世の中の科学に対する関心が低い気がします。2017年は「重力波の観測」「キログラム原器の変更」なんてニュースがありましたが、どれくらいの人が認識しているでしょうか?
 それなりに科学を学んできた私は、こういうニュースに関心があります。ですが、実生活にはまず影響しない内容なので、関心を持っていない人も多いと思います。

 科学に対する関心が低い社会では、科学研究に出資してくれるスポンサーを見つけるのは難しいでしょう。当然、社会の科学力が低下します。大問題です。

 国の科学力を高くしたいのであれば、まず社会の科学に対する関心を、もっと高めるようにしないといけないんだろう、と思います。

2017年12月10日日曜日

街の形

 短い間ですが、京都に住んでいたことがあります。

 京都の街といえば、中国の洛陽などをモデルにした、碁盤の目のような十字の道で知られています。条坊制と言うそうです。
 見た目はとてもわかりやすいですね。
 ですが、住んでみると、それほど便利というわけではありません。理由は、目的地まで最短距離で進めないからです。

 辺の長さが3:4:5の直角三角形を思い浮かべてください。斜めに進めば500mで進めるのに、縦横と進むと700mになるわけです。1.4倍です。20分で行ける道が28分になるわけです。曲がるときに信号待ちしたら、30分になるでしょう。
 道が十字であるが故に、移動距離はどうしても長くなってしまいます。

 街の中心から放射状に道が出ている都市を田園都市と言います。ヨーロッパなどでは、教会や駅を中心とした田園都市がよく見られます。田園都市では、中心に行くのに、ほぼ直線で行けます。そして、目的地はたいてい中心部にあるので、移動が効率的です。

 某NHKの番組ではありませんが、こんなことを考えながら街を歩くのも楽しいものです。

2017年12月3日日曜日

役に立たない勉強をやめたとして

 先日、電車で高校生と思しき一団と遭遇しました。登下校の時間にはよくあることです。お喋りしてたり、スマホをいじってたり。

 で、彼ら彼女らを見ていてふと思ったのですが、高校・大学の勉強って必要でしょうか?

 「社会に出てから、数学や物理が必要ない」なんて意見はよく聞きます。もちろん、人によって必要な知識は異なると思いますが、学校の勉強は得てして実践的ではありません。
 役に立つ勉強ということであれば、「第一印象をよくする方法」とか、「確定申告のやり方」とか、「おいしいレストランの見つけ方」とか、を教えてもらった方がよっぽど実用的です。
 社会に出てからのことを考えると、学校の勉強は役に立たなすぎます。(専門学校はもう少し違うと思いますが。)

 今後、人口減少に伴い、労働人口の減少が予想されています。それなのに、十分な労働力である高校生、大学生が、労働しないで、役に立たない勉強をしているというのは、無駄でしかありません。なんとももったいない。
 私が思うに、現場での仕事は教室での授業よりはるかに勉強になります。役に立たない勉強をさせるより、働いてもらった方が絶対に有意義です。
 (ちなみに、働いて稼いだお金が消費につながれば、経済が活性化するというおまけも期待できます。)

 役に立たない勉強を我慢して大学に行って大卒の資格を取得すると、就職したときに給料が上がります。ですが、大学でぐうたらしていると、中卒や高卒より給料が良くなるというのはおかしな話です。こういう旧態依然としたシステムは早急に見直されるべきだと思います。


 ところで、高校の授業料の無償化が議論されています。極端な言い方になりますが、高校の授業なんて、役に立たない知識を覚えさせられる、ある種の苦行です。授業中に居眠りをしたり、スマホをいじるのも致し方ないと、私は思います。
 授業料の無償化とは、そんな高校の授業料を税金で支払おう、ということです。価値あるものに税金を使うのは当然ですが、今の高校の授業に税金を使う価値があるかには疑問が残ります。

 話が逸れますが、大学受験でしか使わない一時の知識のために、塾や予備校に通うのは、やっぱり時間とお金の無駄だと思います。大卒の資格を得るために、社会に出てから役に立たない勉強をするのは、何かがおかしいです。



 教育の問題は、みんなが経験しているだけに、みんなが興味を持つ問題です。自分が経験したものが基準なので、私の考えには多分に偏見があると思います。それでも、電車に乗っている学生を見て、そんなこんなを考えてしまうわけです。

2017年11月4日土曜日

恥ずかしい小説のモデルにならないように

 ここのところ、神戸製鋼の検査データ改ざん問題が騒がれています。大企業の不祥事を聞くと、いつも悲しくなります。

 ところで、神戸製鋼というと、私は城山三郎の小説「鼠―鈴木商店焼打ち事件」を思い出します。

 鈴木商店は、神戸製鋼や帝人といった大企業の母体となった戦前の商社です。小説では、米騒動や財閥間の争いといった激動の時代の中での、鈴木商店の栄枯盛衰が描かれています。

 城山三郎先生の経済小説は、背景の史実が重厚で、非常に面白いです。モデルとなった人々の偉大さに、いつも尊敬の念を抱きます。

 歴史ある大企業には偉大な業績があるものだし、その裏での不祥事もそれなりにあるものだと思います。それでも、将来、自分たちが恥ずかしい小説のモデルになってしまわないよう、精進したいものです。

2017年9月30日土曜日

人はAIを信じられるか?

 近年、AIの発展が著しいです。

 AIに質問すれば、たいていのことは答えが出てきます。では、その答えを人は信じるでしょうか?

 たとえ答えが正しくても、人はその答えを採用しないことがあります。
 分かりやすい例えで言えば、中世ヨーロッパでは、正しくない天動説が正しいと信じられていました。また、AIが出した正しい答えを人が正しいと納得するとは限りません。(ちなみに、AIが答えを出す過程というのは、複雑過ぎて理解できないらしいです。)
 どんなに正しい答えでも、人がその答えを信じないということは起きるものです。

 人がAIの出す答えを採用しないことがあれば、そのうちAIはそれも学ぶでしょう。AIが人を信じなくなる。。。人が答えを採用しないことを見越して、AIがあえて正しくない答えを出す。。。映画にありそうな展開です。

 最近のAIの発展を聞くと、そんなSFのような話が現実になりそうな気がします。技術の進歩にびっくりです。。

2017年9月24日日曜日

会議の準備

 会議の準備という仕事があります。

 主な内容は資料の作成です。グラフを作ったり、文章を書いたり。ExcelやPowerPointのおかげで、手軽にキレイな資料が作れます。が、やっていると意外に時間がかかるものです。

 作った資料が事前のチェックでやり直しになることもしばしば。で、本番の会議では時間が不足して資料を見てもらえなかったり。よくあることです。

 実に生産性が低いことだと思います。

 無駄になる資料を作っているくらいなら、現場の清掃をしている方がよっぽど生産的です。

 会議の準備に追われて残業が増えるのは、バカらしいことです。ですが、私の知る限り、会議の準備に追われるというのはよくあることです。

 悲しいかな、悲しいかな。

2017年9月17日日曜日

Pigeon's Vision

 昔から不思議に思っていることがあります。

 公園とかに行くと、ハトが地面をつついて餌を探しています。パンくずなんかがあれば、それを食べているのでしょう。

 ・・・ハトはどうやって餌を認識するのでしょうか?

 視覚でしょうか?パンくずと砂粒の見た目に大きな違いはない気がします。

 嗅覚でしょうか?パンくずにはそれほど匂いがない気がします。

 一度、口に入れて、味覚や触覚で認識するのでしょうか?ハトの小さなクチバシがそれほど発達しているようには思えません。


 自分がハトだったと想像してみると、パンくずと砂粒の違いを認識できる気がしません。

 ハトが何を思って地面をつつくのか、、、
 不思議です。


2017年7月30日日曜日

持続可能な開発

 持続可能型社会という言葉を聞く機会が増えました。平たく言ってしまうと、「環境破壊をやめましょう」という意図なのでしょう。悪いことだとは思いません。

 少し考えてみましょう。
 開発とは一般的に何かを作ることです。作ったらものが増えていきます。
 単調に増加するものは必ず発散します。発散してしまったら持続しません。
 つまり、ものを作ってばかりでは安定しないということです。ものを作ることと、ものを捨てることがバランスしないといけません。

 プラスの作用とマイナスの作用をバランスさせて安定化させるというのは、至極普通の考え方だと思います。その考え方を基準にすると、「持続可能な開発」という言葉は、作ることに重点があるようで少し変です。

 「環境に配慮して、ソーラーパネルを設置する」結構なことです。では、そのソーラーパネルに取って代わられる既存の発電設備はどのように廃棄されるのでしょうか? また、そのソーラーパネルは老朽化したときどのように廃棄されるのでしょうか?

 私が思うに、ものを捨てるということは、ものを作るのと同じくらい、難しく、付加価値の高いことです。持続可能性を考えるのであれば、ものを捨てるということの重要性をもっと考えてもいいと思うのですが。。。
 そういう考え方はマイナーな気がします。


クルマを買う話

 東京は駐車場代が高いのでクルマがあると大変ですが、地方に住むならクルマがないと生活が大変です。
 最近、再び地方に引っ越したため、クルマを買おうと思い、いくつかディーラーさんで、試乗させてもらっています。

 クルマもいろいろありますが、今回は、同僚におすすめされた、ルノーのトゥインゴに乗ってきました。

 トゥインゴは、いわゆるコンパクトカーです。見た目はフランス車という感じのオシャレなデザインをしています。
 乗ってみるとすぐに走りの違いに気づきます。トゥインゴはCVTではなくDCTです。そのため、慣れないとギアが変わるタイミングで、クルマがガクッとなります。楽しいです。ギアの変化が好きな人にはおすすめです。
 FRではなくRRというのも特徴的です。小回りが良い反面、後ろに積める荷物は少なめです。
 ルノーはシートにこだわっているとのことで、実際、デザインも座り心地も良かったです。高級感があるわけではないのですが、シンプルで実用的な感じです。
 価格は日本のコンパクトカーと同じ程度です。輸入車であるため、故障とかがあると面倒かもしれません。

 総じて言うと、オシャレで特徴的な走りの素敵なクルマでした。ただ、売れ筋で在庫が少なく、新車を買っても3か月以上待つことになるそうです。。。

 そんなこんなで楽しくクルマを選んでいる今日この頃です。

2017年6月25日日曜日

ふりまわされる学生

 小学校でのプログラミング教育が検討されているそうです。情報化が進んだ昨今、学生が社会に出てからの仕事のことを考えてのことでしょう。
 ですが、プログラミングを追加して、英語を追加してと増やしてばかりでは、学生も先生もパンクしてしまいそうです。

 インターンの採用直結を認めないというニュースがありました。社会に出てからの仕事の前に、学業を優先させるということらしいです。
 先ほどの話と優先順位が逆転しているところは滑稽です。

 以前、博士号を持つ人材を増やそうという政策がありました。海外の学位のシステムを意識した政策なのでしょうが、その結果、就職できないポスドクが増えたという話をよく聞きます。

 こういった話を聞くと、学生がふりまわされているという印象を受けます。

 教育に正解はないでしょう。人それぞれです。しっかり学業を修めた方がいい人もいれば、インターンから直接就職して活躍する人もいるでしょう。
 正解はないからこそ、押し付けだけはやめて欲しいと思います。すべての学生に一つの政策を押し付けるのは、独善的で配慮に欠いたやり方です。


 私は、インターンの採用直結を悪いと思わないので、先のニュースを聞いてびっくりしました。もちろん世の中にはいろいろな裏の事情があるとは思いますが、「学業を優先させる」という一方的な論理を強要するのは、それこそ教育的ではないと強く思った次第です。


クレーマー・クレーマー

 クレームを言うのが苦手です。

 日本人の気質でしょうか。文句を言って不和を生むよりは、我慢することを選んでしまいます。

 ですが、現状に不満があるからこそ、将来の改善につながるものです。正しいクレームは、実は生産的な活動だと思います。
 もちろん、正しくないクレームは諍いを生むだけなので、その加減が難しいのですが。

 願わくば、間違いはきちんと指摘できるようになりたいものです。

情報過多

 テレビのニュースを見ていると、アナウンサーの変わり身にときどき驚かされます。

 中東情勢の話をしていると思えば、アメリカ、北朝鮮、と世界一周して、休む間もなくスポーツの話題に変わったりします。

 アナウンサーの方々は、非常に賢いのだと思います。
 あらゆる話題をさも関心があるように話す様は、少し気持ち悪いくらいです(失礼ですみません)。
 個人的には、「私は大相撲に興味ありません」というくらいでもいいと思います。


 現代はあらゆる情報が集まる時代です。いつの間にか大量の情報が押し寄せてくる時代です。
 ですが、頭の中で処理できる情報の量には限りがあると思います。

 大量の情報にさらされていると、目を回してしまいそうになることがあります。情報の海に溺れているような気分です。頭の中で、大量の情報を処理し続けるのは疲れるんです。

 どうせ頭で処理しきれないのだから、少し情報を遮断して、休憩したいなぁなどと思ったりする今日この頃です。



2017年6月11日日曜日

Excel VBAで数学 8

 Excel VBAを使った簡単な数学プログラミングの続きです。

 今回は組み合わせの計算です。
 学校の数学でさんざん考えた記憶がありますが、コンピュータにやらせるとあっさりしたものです。

 ソースコードは、ご自由にご利用ください。ただし、趣味のプログラムなので、保証はありません。

Option Explicit

Dim row As Long


Public Sub Macro()
    Dim N As Long
    Dim m As Long
    Dim num() As Byte
   
    row = 1
   
    N = 5
    m = 3
    ReDim num(m - 1) As Byte
    'nHm num, N, m, 0
    'nPm num, N, m, 0
    nCm num, N, m, 0
End Sub

'nHm
'重複あり組み合わせ
Public Sub nHm(num() As Byte, N As Long, m As Long, pos As Long)
    Dim i As Long
   
    If pos < m Then
        For i = 0 To N - 1
            num(pos) = i + 1
            nHm num, N, m, pos + 1
        Next i
    ElseIf pos = m Then
        For i = 0 To m - 1
            Sheet1.Cells(row, i + 1) = num(i)
        Next i
       
        row = row + 1
    End If
End Sub

'nPm
'順列 (permutation)
Public Sub nPm(num() As Byte, N As Long, m As Long, pos As Long)
    Dim i As Long
    Dim j As Long
   
    If pos < m Then
        For i = 0 To N - 1
            For j = 0 To pos - 1
                If i + 1 = num(j) Then Exit For
            Next j
           
            If j = pos Then
                num(pos) = i + 1
                nPm num, N, m, pos + 1
            End If
        Next i
    ElseIf pos = m Then
        For i = 0 To m - 1
            Sheet1.Cells(row, i + 1) = num(i)
        Next i
       
        row = row + 1
    End If
End Sub

'nCm
'組み合わせ (combination)
Public Sub nCm(num() As Byte, N As Long, m As Long, pos As Long)
    Dim i As Long
    Dim j As Long
   
    If pos < m Then
        For i = 0 To N - 1
            For j = 0 To pos - 1
                If i + 1 <= num(j) Then Exit For
            Next j
           
            If j = pos Then
                num(pos) = i + 1
                nCm num, N, m, pos + 1
            End If
        Next i
    ElseIf pos = m Then
        For i = 0 To m - 1
            Sheet1.Cells(row, i + 1) = num(i)
        Next i
       
        row = row + 1
    End If
End Sub

Excel VBAで数学 7

 Excel VBAを使った簡単な数学プログラミングの続きです。

 今回は円周率の計算です。
 値は知っていても、実際に計算したことがある人は、多くないのではないでしょうか。
 今回は、Machinの公式というのを使っています。計算機がない時代に手計算をしていた人々のことを思うと、感慨深いです。

 ソースコードは、ご自由にご利用ください。ただし、趣味のプログラムなので、保証はありません。

Option Explicit

'piの計算 (Machinの公式)

Public Sub Macro()
    Dim i As Long
    Dim m As Long
    Dim p As Double
    Dim temp As Double
 
    m = 100
 
    temp = 16 / 5
 
    For i = 0 To 3 * m + 2
        p = p + temp / (2 * i + 1)
        temp = -temp / 5 / 5
    Next i
 
    temp = 4 / 239
 
    For i = 0 To m
        p = p - temp / (2 * i + 1)
        temp = -temp / 239 / 239
    Next i
 
    Sheet1.Cells(1, 1) = p
End Sub

Excel VBAで数学 6

 Excel VBAを使った簡単な数学プログラミングの続きです。

 今回はネイピア数です。自然対数の底という言った方が普通な気がします。
 普通のプログラミングだと、変数の型に制限があるので、10桁くらいしか求まりません。もっと桁数を増やすにはテクニックが必要です。

 ソースコードは、ご自由にご利用ください。ただし、趣味のプログラムなので、保証はありません。

Option Explicit

'eの計算

Public Sub Macro()
    Dim i As Long
    Dim e As Double
    Dim temp As Double
   
    e = 1
    temp = 1
   
    For i = 1 To 100
        temp = temp / i
        e = e + temp
    Next i
   
    Sheet1.Cells(1, 1) = e
End Sub

Excel VBAで数学 5

 Excel VBAを使った簡単な数学プログラミングの続きです。

 今回はピタゴラス数です。三平方の定理、ピタゴラスの定理と呼ばれるやつです。
 前回のEuclidの互除法を利用しています。
 (3,4,5) (5,12,13)くらいは有名ですが、(777,464,905)なんかは覚えている人は稀でしょう。

 ソースコードは、ご自由にご利用ください。ただし、趣味のプログラムなので、保証はありません。

Option Explicit

'ピタゴラス数

Public Sub Macro()
    Dim i As Long
    Dim m As Long
    Dim N As Long
    Dim row As Long
   
    row = 1
   
    For i = 1 To 100
        m = 1
        N = 2 * i - 2
       
        Do While m < N
            Sheet1.Cells(row, 1) = m
            Sheet1.Cells(row, 2) = N
           
            If Euclidean(m, N) = 1 Then
                Sheet1.Cells(row, 3) = N * N - m * m
                Sheet1.Cells(row, 4) = 2 * m * N
                Sheet1.Cells(row, 5) = N * N + m * m
            End If
           
            m = m + 1
            N = N - 1
            row = row + 1
        Loop
    Next i
End Sub

Public Function Euclidean(x As Long, y As Long) As Long
    Dim a As Long
    Dim b As Long
    Dim r As Long
   
    If x < y Then
        a = y
        b = x
    Else
        a = x
        b = y
    End If
   
    r = a Mod b
   
    Do While 0 < r
        a = b
        b = r
        r = a Mod b
    Loop
   
    Euclidean = b
End Function


Excel VBAで数学 4

 Excel VBAを使った簡単な数学プログラミングの続きです。

 今回はEuclidの互除法です。
 アルゴリズムの基本という感じがします。実は、次回への布石です。

 ソースコードは、ご自由にご利用ください。ただし、趣味のプログラムなので、保証はありません。

Option Explicit

'Euclidの互除法

Public Sub Macro()
    Dim x As Long
    Dim y As Long
   
    x = 123
    y = 333
   
    Sheet1.Cells(1, 1) = x
    Sheet1.Cells(1, 2) = y
    Sheet1.Cells(1, 3) = Euclidean(x, y)
End Sub

Public Function Euclidean(x As Long, y As Long) As Long
    Dim a As Long
    Dim b As Long
    Dim r As Long
   
    If x < y Then
        a = y
        b = x
    Else
        a = x
        b = y
    End If
   
    r = a Mod b
   
    Do While 0 < r
        a = b
        b = r
        r = a Mod b
    Loop
   
    Euclidean = b
End Function

Excel VBAで数学 3

 Excel VBAを使った簡単な数学プログラミングの続きです。

 今回は完全数です。
 実はこの一連の数学プログラミングは、完全数を探してみたくて始めたものです。6, 28, 496, 8128くらいはすぐ見つかりました。数字が大きくなると計算時間がかかるので、なかなか見つからないです。

 ソースコードは、ご自由にご利用ください。ただし、趣味のプログラムなので、保証はありません。

Option Explicit

'完全数を調べる

Public Sub Macro()
    Dim i As Long
    
    For i = 1 To 100
        Sheet1.Cells(i, 1) = i
        
        If isPerfect(i) Then
            Sheet1.Cells(i, 2) = "Perfect"
        End If
    Next i
End Sub

Public Function isPerfect(x As Long) As Boolean
    Dim i As Long
    Dim sum As Long
    
    isPerfect = False
    sum = 0
    
    For i = 1 To x - 1
        If x Mod i = 0 Then
            sum = sum + i
        End If
    Next i
    
    If sum = x Then
        isPerfect = True
    End If
End Function

Excel VBAで数学 2

 Excel VBAを使った簡単な数学プログラミングの続きです。

 今回は素因数分解です。
 小学校の頃にさんざんやった記憶があります。

 ソースコードは、ご自由にご利用ください。ただし、趣味のプログラムなので、保証はありません。

Option Explicit

'素因数分解

Public Sub Macro()
    Dim i As Long
    Dim j As Long
    Dim f As Collection
    
    For i = 1 To 100
        Sheet1.Cells(i, 1) = i
        Set f = Factorize(i)
        
        For j = 1 To f.count
            Sheet1.Cells(i, j + 1) = f(j)
        Next j
    Next i
End Sub

Public Function Factorize(x As Long) As Collection
    Set Factorize = New Collection
    
    Dim temp As Long
    Dim factor As Long
    
    temp = x
    factor = 2
    
    Do While (factor <= temp)
        If temp Mod factor = 0 Then
            temp = temp \ factor
            Factorize.Add factor
        Else
            factor = factor + 1
        End If
    Loop
End Function

Excel VBAで数学 1

 なんでも学校でのプログラミングの授業が計画されているそうです。文部科学省のWebサイトを覗いてみたら、なかなか高度な内容だったので、びっくりです。
 先生は大変だろうなぁ、などと思っています。

 それはそれとして、Excel VBAで簡単な数学のプログラムを作ってみました。個人的には、こういうプログラムは教育的だと思っています。

 まずは素数の判定プログラムです。
 999611と999613のように双子の素数を見つけたりすると少し楽しいです。
 実際に実行してみると、計算時間を体感できます。例えば10000桁の素数を探すことがいかに大変かが分かります。素数は無限に存在しますが、その意味を体感している感じです。

 ソースコードは、ご自由にご利用ください。ただし、趣味のプログラムなので、保証はありません。

Option Explicit

'素数を調べる

Public Sub Macro()
    Dim i As Long
   
    For i = 1 To 100
        Sheet1.Cells(i, 1) = i
       
        If isPrime(i) Then
            Sheet1.Cells(i, 2) = "Prime"
        End If
    Next i
End Sub

Public Function isPrime(x As Long) As Boolean
    Dim i As Long
    Dim max As Long
   
    isPrime = True
   
    max = CLng(Math.Sqr(x))
   
    For i = 2 To max
        If x Mod i = 0 Then
            isPrime = False
            i = max
        End If
    Next i
End Function



規則正しい生活

「規則正しい生活を心がけなさい」とは、よく言われることです。

 ですが、よくよく考えてみると、規則正しい生活をしているのは、現代人だけなのではないでしょうか?
 毎朝同じ時間に起きて、会社や学校に行って、家に帰って、しばらくしたら寝る。そんな単調な生活を送っているのは、現代人だけな気がします。

 人類が狩猟や農耕をしていた時代、生活は自然のリズムに従っていたはずです。自然のリズムは単調ではなく、晴れた日もあれば、雨の日もあります。生活は不規則を強いられていたでしょう。

 そう考えると、単調すぎる生活よりは、少し不規則な生活の方が、自然に近い生活なのかもしれません。規則正しすぎる生活より、少し不規則な生活の方が、刺激があってよいかもしれません。

 近頃、不規則な生活が続いているせいか、そんなことを考えています。

 もちろん、度が過ぎた不摂生な生活はダメだと思いますが。

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

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