以下の内容はhttps://memorandums.hatenablog.com/entry/2026/01/15/110339より取得しました。


Copilot手遊び

明後日はいよいよ共通テスト。とにかく体調を整える意識だけは高い状況です。普段はマスクしてなかったのですがこの1ヶ月くらいは電車も学内等でもするようにしています。とにかくインフルとか風邪とかにかからずに当日を迎えたい、それだけです。嫌だな。。。とは思いますが、たぶん、社会人をやっているとこういう緊張する場面が少なからずあるのでピシっとしようと思うのでしょうね。退職して家にいたら。。。たぶんこういう刺激がなくなるのでしょう。刺激大事です。

さて、今日は授業無しデー。ちょっ遊んでみたのでブログにメモしておきたいところです。はい。

まず、以下の動画を拝聴しました。ふーん。。。という感じでした。

www.youtube.com

ちょっと脱線します。北海道出身なんですが、子供の頃、周りの人の多くは巨人ファンでした(今は日ハムでしょうね)。ホーム球団がないこと。あとテレビは巨人戦ばかりだったからでしょう。そんななか僕は阪神ファンでした。野球は本当に下手くそ(しかも努力もしない)でしたが当時のヒーロー掛布のユニホームを買ってもらったのを今でも覚えています。期待を見事に裏切りましたけど。

ということもあり(どういうことだよ?)一強ってあまり好きじゃないです。特にPythonが主要言語ってのがあまりにあまりだ。。。と思っていた時期がありました。今は違います。便利ですし素晴らしい言語と思っていますが。でも、これまで多くの言語が流行っては薄れていったのを見てきましたからPythonだって例外はないというのは簡単に想像できます。

確かに、Copilotで遊んでいると「これでプログラムしているっていうのか?」という疑問は常に浮かびますし、慣れてくれば「もはやこれはプログラミングというよりは要求設計者(かっこよくいえばデザイナ)だよな」とも感じます。それがいいとか悪いとかではなくです。そうなっていくことを体感として実感しています。

で、やっと話しを戻します。

動画がいうには、Python一強だけど生成AIでコード生成する場合はAIが得意とする言語が選択されていくだろうという話しです。Pythonコードをたくさん食わせればいいじゃん。。。とは思いますが、言語が何であっても要求を満たしていればいいわけで。むしろコードが見えなくてもいいくらいの存在になっているような気さえします。Pythonの代わりにRust、Go、C++、Typescriptが使われていくのでは?ということでした。ちょっと意外なのは関数系の言語が出てこないところですが。たぶん学習データの多さによるのでしょうか。

じゃ、やってみよう、ということで少しCopilotと遊んでみたんです。

なので、ここからやっと本題です。使用するのはVSCode上でGithub Copilot(使用AgentはAutoを選択)です。マシンはMacですが関係ないですね。

まず、プロンプトです。

midiファイルを読み込んで再生するpythonアプリケーションを作ってください。パッケージ管理はuvを使ってください。

これだけで、5分くらいで以下の100行のコードが完成しました。midoライブラリのおかげなんでしょう。とても短いコードです。

import argparse
import sys
import time
from pathlib import Path

import mido
import pygame


class MidiPlayer:
    """MIDIファイルを再生するクラス"""
    
    def __init__(self):
        """Pygameの初期化"""
        pygame.init()
        pygame.mixer.init()
        
    def play_midi(self, midi_file: str):
        """
        MIDIファイルを再生する
        
        Args:
            midi_file: 再生するMIDIファイルのパス
        """
        # ファイルの存在確認
        path = Path(midi_file)
        if not path.exists():
            print(f"エラー: ファイルが見つかりません: {midi_file}")
            return
        
        if not path.suffix.lower() in ['.mid', '.midi']:
            print(f"警告: MIDIファイル以外の形式です: {midi_file}")
        
        try:
            # MIDIファイルの情報を表示
            mid = mido.MidiFile(midi_file)
            print(f"\n=== MIDIファイル情報 ===")
            print(f"ファイル: {path.name}")
            print(f"トラック数: {len(mid.tracks)}")
            print(f"総時間: {mid.length:.2f}秒")
            print(f"タイプ: {mid.type}")
            print(f"TPB (Ticks Per Beat): {mid.ticks_per_beat}")
            
            # トラック情報を表示
            print(f"\n=== トラック情報 ===")
            for i, track in enumerate(mid.tracks):
                print(f"トラック {i}: {track.name if track.name else '(名前なし)'} - {len(track)}メッセージ")
            
            # Pygameで再生
            print(f"\n再生中...\n")
            pygame.mixer.music.load(midi_file)
            pygame.mixer.music.play()
            
            # 再生が終わるまで待機
            while pygame.mixer.music.get_busy():
                time.sleep(0.1)
            
            print("\n再生完了")
            
        except Exception as e:
            print(f"エラー: {e}")
            sys.exit(1)
    
    def stop(self):
        """再生を停止してクリーンアップ"""
        pygame.mixer.music.stop()
        pygame.mixer.quit()
        pygame.quit()


def main():
    """メイン関数"""
    parser = argparse.ArgumentParser(
        description="MIDIファイルを再生するシンプルなプレイヤー",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
使用例:
  python main.py music.mid
  python main.py path/to/song.midi
        """
    )
    parser.add_argument(
        'midi_file',
        type=str,
        help='再生するMIDIファイルのパス'
    )
    
    args = parser.parse_args()
    
    player = MidiPlayer()
    
    try:
        player.play_midi(args.midi_file)
    except KeyboardInterrupt:
        print("\n\n中断されました")
    finally:
        player.stop()


if __name__ == "__main__":
    main()

実行方法は以下の通りです。uvを使っています。uvは本当に楽ですね。早いし。

uv run python main.py hoge.mid

で、お次はRustです。以下がプロンプト、ほとんど先程と同じですね。

midiファイルを読み込んで再生するrustアプリケーションを作ってください。

また5分くらいで以下の158行のコードが完成しました。入れ物をとても短いコードです。

use std::{env, fs, thread, time::Duration};

use anyhow::{anyhow, bail, Context, Result};
use midir::{MidiOutput, MidiOutputConnection};
use midly::{MetaMessage, MidiMessage, Smf, Timing, TrackEventKind};

#[derive(Debug)]
enum EventKind {
    Midi(Vec<u8>),
    Tempo(u32),
}

#[derive(Debug)]
struct PlaybackEvent {
    tick: u64,
    order: u64,
    kind: EventKind,
}

fn main() -> Result<()> {
    let mut args = env::args().skip(1);
    let midi_path = args
        .next()
        .context("usage: midi_player <midi_file.mid> [port_index]")?;
    let port_index = args
        .next()
        .map(|v| v.parse::<usize>().context("port_index must be a number"))
        .transpose()?;

    let data = fs::read(&midi_path).with_context(|| format!("failed to read {midi_path}"))?;
    let (events, ppqn) = load_events(&data)?;

    let conn = open_midi_output(port_index)?;
    println!("Playing {midi_path}...");
    play(events, ppqn, conn)?;
    println!("Done.");
    Ok(())
}

fn open_midi_output(port_index: Option<usize>) -> Result<MidiOutputConnection> {
    let midi_out = MidiOutput::new("midi_player")?;
    let ports = midi_out.ports();
    if ports.is_empty() {
        bail!("No MIDI output ports found.");
    }

    let idx = port_index.unwrap_or(0);
    if idx >= ports.len() {
        let names: Vec<String> = ports
            .iter()
            .enumerate()
            .map(|(i, p)| format!("{i}: {}", midi_out.port_name(p).unwrap_or_else(|_| "Unknown".into())))
            .collect();
        bail!("Port index {idx} is out of range. Available ports: {}", names.join(", "));
    }

    let port = &ports[idx];
    let port_name = midi_out
        .port_name(port)
        .unwrap_or_else(|_| "Unknown".to_string());
    println!("Using port {idx}: {port_name}");
    let conn = midi_out
        .connect(port, "midi_player")
        .map_err(|e| anyhow!("failed to open port {idx}: {e}"))?;
    Ok(conn)
}

fn load_events(data: &[u8]) -> Result<(Vec<PlaybackEvent>, u16)> {
    let smf = Smf::parse(data)?;
    let ppqn = match smf.header.timing {
        Timing::Metrical(t) => t.as_int(),
        _ => bail!("SMPTE time code is not supported"),
    };

    let mut events = Vec::new();
    let mut order_counter = 0u64;

    for track in &smf.tracks {
        let mut abs_tick = 0u64;
        for event in track {
            abs_tick += event.delta.as_int() as u64;
            match event.kind {
                TrackEventKind::Midi { channel, message } => {
                    if let Some(msg) = midi_bytes(channel, message) {
                        events.push(PlaybackEvent {
                            tick: abs_tick,
                            order: order_counter,
                            kind: EventKind::Midi(msg),
                        });
                        order_counter += 1;
                    }
                }
                TrackEventKind::Meta(MetaMessage::Tempo(us_per_beat)) => {
                    events.push(PlaybackEvent {
                        tick: abs_tick,
                        order: order_counter,
                        kind: EventKind::Tempo(us_per_beat.as_int()),
                    });
                    order_counter += 1;
                }
                _ => {}
            }
        }
    }

    Ok((events, ppqn))
}

fn play(mut events: Vec<PlaybackEvent>, ppqn: u16, mut conn: MidiOutputConnection) -> Result<()> {
    events.sort_by(|a, b| a.tick.cmp(&b.tick).then(a.order.cmp(&b.order)));

    let ticks_per_beat = ppqn as u64;
    let mut current_tick = 0u64;
    let mut micros_per_beat = 500_000u64; // default 120 BPM

    for ev in events {
        let delta_ticks = ev.tick.saturating_sub(current_tick);
        if delta_ticks > 0 {
            let delta_micros = delta_ticks * micros_per_beat / ticks_per_beat;
            if delta_micros > 0 {
                thread::sleep(Duration::from_micros(delta_micros));
            }
            current_tick = ev.tick;
        }

        match ev.kind {
            EventKind::Midi(msg) => {
                conn.send(&msg)?;
            }
            EventKind::Tempo(new_tempo) => {
                micros_per_beat = new_tempo as u64;
            }
        }
    }

    Ok(())
}

fn midi_bytes(channel: midly::num::u4, message: MidiMessage) -> Option<Vec<u8>> {
    let ch = channel.as_int();
    let status_base = ch & 0x0F;
    match message {
        MidiMessage::NoteOff { key, vel } => Some(vec![0x80 | status_base, key.as_int(), vel.as_int()]),
        MidiMessage::NoteOn { key, vel } => Some(vec![0x90 | status_base, key.as_int(), vel.as_int()]),
        MidiMessage::Aftertouch { key, vel } => Some(vec![0xA0 | status_base, key.as_int(), vel.as_int()]),
        MidiMessage::Controller { controller, value } => {
            Some(vec![0xB0 | status_base, controller.as_int(), value.as_int()])
        }
        MidiMessage::ProgramChange { program } => Some(vec![0xC0 | status_base, program.as_int()]),
        MidiMessage::ChannelAftertouch { vel } => Some(vec![0xD0 | status_base, vel.as_int()]),
        MidiMessage::PitchBend { bend } => {
            let value = bend.as_int();
            let lsb = (value & 0x7F) as u8;
            let msb = ((value >> 7) & 0x7F) as u8;
            Some(vec![0xE0 | status_base, lsb, msb])
        }
    }
}

同じ機能をもつプログラムファイルについて比較してみました。Rustで記述するとPythonと比べて、行数で1.5倍、単語数で3.1倍です。記述量は多くなりますね。でも、コード生成時間が変わらないとすれば実行速度・堅牢性の恩恵が受けられるなら確かにRustの選択になるケースが増えていくというのはわかる気がします。そもそもインタプリタは人間が少しずつ手で入力して作っていくことを想定した言語実装と思いますし(コンパルする手間と時間を駆逐したかった)。プロトタイプレベルも生成AIを使えば時間的なコストは抑えられますし。この辺はビジネスの世界の話しなので適当なことしか言えませんが。

行数 単語数 バイト数 ファイル名
102 163 2891 main.py
158 (1.6倍) 507 (3.1倍) 5314 (1.8倍) main.rs

で、僕の興味はやはり教育です。この仕組みをどう教育に活かすか?プログラミング言語は知らなくていい時代が来ないこともない気もするけど「一応」は読めた方がいいし作れた方がいいという感じだと勉強する方もどこまで本気を出せばいいのかわからないですよね。。。ほんと。一行一行作ることは考えなくていいのかもしれませんし。

で、ちょっと思いつきですが、生成してもらったコードを動かして「あー面白かった」で終わるのは教育とは言えないので、じゃ、これを題材として学習するというのはすぐに思い浮かびますね。例えば、コードリーディングです。ウォークスルーとでもいいましょうか。コードをみんなで読み合わせていく感じです。わからなければその場でAIに聞けばいい感じですね。

コードリーディング系の本はいくつか出ていますが、なかなか初心者が読むのはハードなものが多いと思います。また初心者が読みたいと思えるような本も。つまりコードリーディングの授業をしたくてもそうした適度なソースコードがオープンソースの時代とはいえ難しかったんですね。。。それが生成AIのお陰でオンデマンドですぐに出力できます、これは活かさない手はないと思うのですね。

しかもどんなプログラミング言語だっていけちゃいます。上のように静的解析して比較したり、モジュール分析したり、実行速度を比較してみたり、そういうプログラム解析実験もできるでしょう。

ただ、ちょっとやってみたのですが、Copilotにそのまま「説明して」といってもいまいちな回答でした。

以下、プロンプトです。

rustを勉強したいです。プログラミング経験者(C、Java、Ruby)がmain.rsを理解するための説明をお願いできますか?

回答が以下。ちょっと違いますね。。。こういうイメージじゃないし、一応、Rustも少し勉強したことがありますが、これじゃ理解が難しい。。。密度が高すぎです。

- 実行フロー: main()でCLI引数を取得 → MIDIバイト列読込 → load_eventsでSMFをパースして時刻付きイベント列に展開 → open_midi_outputでMIDI出力ポートを開く → playでテンポを反映しながらリアルタイムに送信 main.rs:18-137。
- エラー処理: anyhow::Resultで早期リターン、contextで失敗時にメッセージ付与、bail!で即エラー main.rs:20-60。
- MIDIポート選択: midir::MidiOutputからポート一覧を取得し、指定インデックス(未指定なら0)を検証して接続。接続失敗はanyhow!で包む main.rs:62-88。
- MIDIパース: midly::Smf::parseでSMF読込。タイムベースはPPQNのみ対応(SMPTEは弾く)。各トラックをデルタタイムで累積し、TrackEventKind::MidiとMetaMessage::Tempoを抽出してPlaybackEventに格納 main.rs:90-127。
- スケジューリング: イベントをtick順にソートし、現在tickとの差分をmicros_per_beatとPPQNでマイクロ秒に換算してsleep、その後conn.sendでMIDI送信。テンポイベントでmicros_per_beatを更新するので途中テンポ変更に追従 main.rs:129-175。
- MIDIメッセージ組立: midi_bytesでchannelメッセージをステータスバイト+データバイトに変換。Note On/Off, Aftertouch, CC, Program Change, Channel Aftertouch, Pitch Bendをサポート(ピッチベンドは14bitをLSB/MSB分割) main.rs:177-201。
- 型のポイント: midly::num::u4でチャンネル(0-15)、as_int()でu8取得。Timing::MetricalでPPQN取得。Tempoはμs/beat。
- Java/C視点での差分: エラーハンドリングはResultと?で伝搬、matchで代数的データ型を分岐。所有権はVec<PlaybackEvent>をplayにムーブ。thread::sleepは標準のブロッキング。
- Ruby視点: 例外ではなく戻り値で制御、パターンマッチで型安全に分岐。タプル返し (Vec<PlaybackEvent>, u16) で複数値を戻す。
- 課題/発展: 1) SMPTE対応、2) NoteOn vel=0をNoteOffに正規化、3) CLIでポート一覧表示、4) メトロノーム/ループ、5) スレッドと高精度タイマー(Instantとbusy-wait回避)、6) 音源内蔵用にソフトシンセ(例:rimd+rodioや外部VST/サウンドフォント)。

GeminiとかChatGPTとかClaudeとかにコードを貼り付けてお願いしましたが似たりよったりでした。

NotebookLMにコードを貼り付けて動画解説をさせてみました。これまでちょっと違うな。。。と思いますが、なんか「プログラミングって楽しいかも?」と思わせることができるかもですね。エンジニア向きではないでしょうけど😁

youtu.be

で、ちょっと考えました。役割を与えてから解説させてみました。

これはまあまあの回答でした。設定をもう少し詳しく与えてみて、分割してみると使えるかもしれません。というか、一斉放送的な授業を想定したコンテンツではなく、やはり生成AIでの学習は「インタラクティブ」にありますよね。。。ソースコードを見ながらメモしていったり、理解できなかったところを局所的に指して聞いているとか、そういうことができる拡張機能かツールがあるといいですね。

以下から回答です。長いですが、そのまま貼り付けます。参考まで。

あなたは情報系の大学教員です。プログラミング言語が専門です。Rustを学習中の学生にわかるように少しずつこのコードを説明してください。少しずつではありますが、もちろん全体を理解できるように説明してください。学生自身が自分でも似たようなプログラムを作ろうとしたときにもできるように説明してください。

Rustプログラミング講座: MIDIプレイヤーを理解しよう

それでは、このMIDIプレイヤーのコードを、段階的に理解していきましょう。最終的には、皆さんが自分でも似たようなプログラムを書けるようになることを目指します。


第1回: プログラムの全体像と必要な知識

1.1 このプログラムは何をするのか?

このプログラムはMIDIファイルを読み込んで、MIDI音源に送信して音楽を再生するプログラムです。

流れはこうです:

MIDIファイル → 読み込み → 解析 → タイミング計算 → MIDI出力デバイスへ送信

1.2 使用している外部ライブラリ(クレート)

use std::{env, fs, thread, time::Duration};
use anyhow::{anyhow, bail, Context, Result};
use midir::{MidiOutput, MidiOutputConnection};
use midly::{MetaMessage, MidiMessage, Smf, Timing, TrackEventKind};

標準ライブラリ(std): - env: コマンドライン引数の取得 - fs: ファイル読み込み - thread: スリープ処理 - time::Duration: 時間間隔の表現

外部クレート: - anyhow: エラー処理を簡単にする - midir: MIDI出力デバイスとの通信 - midly: MIDIファイルの解析

1.3 Cargo.tomlの設定

このプログラムを動かすには、Cargo.tomlに以下の依存関係が必要です:

[dependencies]
anyhow = "1.0"
midir = "0.9"
midly = "0.5"

第2回: データ構造の設計

プログラムで扱うデータの「型」を定義します。

2.1 EventKind - イベントの種類

#[derive(Debug)]
enum EventKind {
    Midi(Vec<u8>),  // MIDI演奏データ
    Tempo(u32),     // テンポ変更情報
}

解説: - enumは「いくつかの選択肢のうちどれか」を表す型です - Midiバリアント: 実際の演奏データ(ノートオン、ノートオフなど)をバイト列で保持 - Tempoバリアント: テンポ変更情報を保持(単位: マイクロ秒/拍) - #[derive(Debug)]: デバッグ出力を可能にする

なぜenumを使うのか? MIDIファイルには「音を鳴らす」イベントと「テンポを変える」イベントが混在しています。これらを統一的に扱うためにenumを使います。

2.2 PlaybackEvent - 再生イベント

#[derive(Debug)]
struct PlaybackEvent {
    tick: u64,        // いつ再生するか(MIDIの時間単位)
    order: u64,       // 同じtickのイベントの順序
    kind: EventKind,  // イベントの種類
}

解説: - structは複数のデータをまとめる型 - tick: MIDIファイル内の時刻(後で実時間に変換します) - order: 同じタイミングで複数のイベントがある場合の順序保証 - kind: 上で定義したEventKind

設計のポイント: MIDIファイルでは時刻が「tick」という単位で記録されています。これを再生時に実時間(マイクロ秒)に変換する必要があります。


第3回: main関数 - プログラムの入り口

3.1 戻り値の型 Result<()>

fn main() -> Result<()> {
    // ...
    Ok(())
}

解説: - 通常、main関数はfn main() {}ですが、ここではResult<()>を返しています - これにより、エラーが発生したときにプログラムを終了できます - Result<()>は「成功したら何も返さない(())、失敗したらエラーを返す」という意味 - 最後のOk(())で「正常終了」を示します

3.2 コマンドライン引数の取得

let mut args = env::args().skip(1);
let midi_path = args
    .next()
    .context("usage: midi_player <midi_file.mid> [port_index]")?;

詳しく見ていきましょう:

let mut args = env::args().skip(1);
  • env::args(): コマンドライン引数をイテレータとして取得
  • .skip(1): 最初の引数(プログラム名自体)をスキップ
  • mut: 後で.next()を呼ぶため可変
let midi_path = args.next().context("...")? ;
  • .next(): イテレータから次の要素を取得(Optionを返す)
  • .context("..."): エラーメッセージを追加(anyhowクレートの機能)
  • ?: エラーだったら即座にreturnする演算子

使い方の例:

$ cargo run song.mid
$ cargo run song.mid 1

3.3 ポート番号の取得(オプション)

let port_index = args
    .next()
    .map(|v| v.parse::<usize>().context("port_index must be a number"))
    .transpose()?;

これは少し複雑です。段階的に理解しましょう:

  1. args.next(): 次の引数を取得 → Option<String>
  2. .map(...): 値があれば関数を適用
  3. v.parse::<usize>(): 文字列を数値に変換 → Result<usize, ...>
  4. ここまでで型は Option<Result<usize, ...>>
  5. .transpose(): Option<Result<T>>Result<Option<T>>に変換
  6. ?: エラーチェック

なぜこんなことをするのか? 「引数がない(Option::None)」は正常ですが、「引数があるけど数値じゃない」はエラーです。この2つを区別するためです。


第4回: ファイル読み込みと処理の流れ

4.1 MIDIファイルの読み込み

let data = fs::read(&midi_path)
    .with_context(|| format!("failed to read {midi_path}"))?;

解説: - fs::read(): ファイルを読み込んでバイト列(Vec<u8>)として返す - .with_context(|| ...): エラー時のメッセージをクロージャで生成 - ||はクロージャ(無名関数)の記法 - format!マクロで文字列を組み立て - ?: エラー処理

クロージャについて:

|| format!("failed to read {midi_path}")

これは「引数なしで文字列を返す関数」です。なぜ直接文字列を渡さないのかというと、エラーが起きたときだけメッセージを作成するため効率的だからです。

4.2 イベントの読み込みと再生

let (events, ppqn) = load_events(&data)?;
let conn = open_midi_output(port_index)?;
println!("Playing {midi_path}...");
play(events, ppqn, conn)?;
println!("Done.");
Ok(())

処理の流れ: 1. load_events: MIDIファイルを解析してイベントリストに変換 2. open_midi_output: MIDI出力デバイスに接続 3. play: 実際に再生 4. Ok(()): 正常終了

タプルの分解代入:

let (events, ppqn) = load_events(&data)?;

load_eventsは2つの値を返すので、それを別々の変数に代入しています。


第5回: MIDI出力デバイスを開く

5.1 関数のシグネチャ

fn open_midi_output(port_index: Option<usize>) -> Result<MidiOutputConnection>

引数: - port_index: Option<usize>: ポート番号(あるかないか)

戻り値: - Result<MidiOutputConnection>: 成功したら接続オブジェクト、失敗したらエラー

5.2 MIDI出力の初期化

let midi_out = MidiOutput::new("midi_player")?;
let ports = midi_out.ports();
if ports.is_empty() {
    bail!("No MIDI output ports found.");
}

解説: - MidiOutput::new(): MIDI出力システムを初期化 - .ports(): 利用可能なMIDI出力ポートのリストを取得 - bail!: エラーを返して即座に関数を終了(anyhowマクロ)

5.3 ポート番号の決定

let idx = port_index.unwrap_or(0);
if idx >= ports.len() {
    // エラー処理...
}

unwrap_orについて: - Option<T>のメソッド - 値があればその値、なければ指定したデフォルト値を返す - ここでは「ポート番号の指定がなければ0番を使う」という意味

5.4 エラーメッセージの生成

let names: Vec<String> = ports
    .iter()
    .enumerate()
    .map(|(i, p)| format!("{i}: {}", midi_out.port_name(p).unwrap_or_else(|_| "Unknown".into())))
    .collect();
bail!("Port index {idx} is out of range. Available ports: {}", names.join(", "));

イテレータの連鎖を理解しましょう:

  1. ports.iter(): ポートのリストを走査
  2. .enumerate(): インデックス付きで走査 → (0, port0), (1, port1), ...
  3. .map(|(i, p)| ...): 各要素を変換
    • (i, p)でタプルを分解
    • ポート名を取得して文字列を作成
  4. .collect(): 結果をVec<String>に集める

unwrap_or_elseについて: - Result<T>Option<T>のメソッド - エラー/Noneの場合、クロージャを実行してデフォルト値を作る - |_|は「引数を使わない」という意味

5.6 接続の確立

let port = &ports[idx];
let port_name = midi_out.port_name(port).unwrap_or_else(|_| "Unknown".to_string());
println!("Using port {idx}: {port_name}");

let conn = midi_out
    .connect(port, "midi_player")
    .map_err(|e| anyhow!("failed to open port {idx}: {e}"))?;
Ok(conn)

.map_err()について: - Result<T, E>のメソッド - エラーの型を変換する - ここではmidirのエラーをanyhowのエラーに変換


第6回: MIDIファイルの解析 (1/2)

6.1 関数のシグネチャと基本構造

fn load_events(data: &[u8]) -> Result<(Vec<PlaybackEvent>, u16)> {
    let smf = Smf::parse(data)?;
    let ppqn = match smf.header.timing {
        Timing::Metrical(t) => t.as_int(),
        _ => bail!("SMPTE time code is not supported"),
    };
    // ...
}

引数と戻り値: - 引数: &[u8] = バイトスライス(ファイルデータ) - 戻り値: (Vec<PlaybackEvent>, u16) = イベントリストとPPQN

PPQNとは? - Pulses Per Quarter Note - 「4分音符あたりのtick数」 - 例: PPQN=480なら、4分音符は480 tick

6.2 SMFのパース

let smf = Smf::parse(data)?;
  • Smf: Standard MIDI Fileの略
  • midlyクレートがMIDIファイルを解析してくれる

SMFの構造:

SMF
├── header (フォーマット、トラック数、タイミング情報)
└── tracks (複数のトラック)
    └── track (イベントのリスト)
        └── event (デルタタイム + イベント本体)

6.3 match式でタイミング情報を取得

let ppqn = match smf.header.timing {
    Timing::Metrical(t) => t.as_int(),
    _ => bail!("SMPTE time code is not supported"),
};

match式について: - Rustの強力なパターンマッチング - Timing::Metrical(t): メトリカルタイミングの場合、値をtに束縛 - _: それ以外のすべてのパターン(ワイルドカード)

MIDIのタイミング方式: 1. Metrical: 音楽的な時間(拍ベース) → 一般的 2. SMPTE: 実時間ベース → 映像同期用


第7回: MIDIファイルの解析 (2/2)

7.1 イベント収集の準備

let mut events = Vec::new();
let mut order_counter = 0u64;
  • events: 最終的に返すイベントリスト
  • order_counter: 同じtickのイベントの順序を記録

7.2 トラックとイベントの走査

for track in &smf.tracks {
    let mut abs_tick = 0u64;
    for event in track {
        abs_tick += event.delta.as_int() as u64;
        // イベント処理...
    }
}

重要な概念: デルタタイムと絶対タイム:

MIDIファイルでは、イベントの時刻は「前のイベントからの差分(デルタタイム)」で記録されています。

イベント1: delta=0,   abs=0
イベント2: delta=100, abs=100
イベント3: delta=50,  abs=150

そのため、abs_tick += event.deltaで絶対時刻に変換します。

7.3 MIDIメッセージの処理

match event.kind {
    TrackEventKind::Midi { channel, message } => {
        if let Some(msg) = midi_bytes(channel, message) {
            events.push(PlaybackEvent {
                tick: abs_tick,
                order: order_counter,
                kind: EventKind::Midi(msg),
            });
            order_counter += 1;
        }
    }
    // ...
}

if let構文について:

if let Some(msg) = midi_bytes(channel, message) {
    // msgを使った処理
}

これは以下と同じ意味です:

match midi_bytes(channel, message) {
    Some(msg) => {
        // msgを使った処理
    }
    None => {} // 何もしない
}

なぜif letを使うのか? Noneの場合は何もしないので、matchよりも簡潔に書けます。

7.4 テンポメッセージの処理

TrackEventKind::Meta(MetaMessage::Tempo(us_per_beat)) => {
    events.push(PlaybackEvent {
        tick: abs_tick,
        order: order_counter,
        kind: EventKind::Tempo(us_per_beat.as_int()),
    });
    order_counter += 1;
}

テンポの単位: - us_per_beat: マイクロ秒/拍 - 例: 500,000μs/拍 = 0.5秒/拍 = 120 BPM

BPMとの関係:

BPM = 60,000,000 / us_per_beat

7.5 その他のイベントは無視

_ => {}

MIDIファイルには他にも様々なメタイベント(歌詞、マーカーなど)がありますが、このプログラムでは再生に必要な情報だけを扱います。

7.6 結果を返す

Ok((events, ppqn))
  • イベントリストとPPQNをタプルで返す
  • Ok()で包むことでResult型に

第8回: MIDIバイト列への変換

8.1 関数のシグネチャ

fn midi_bytes(channel: midly::num::u4, message: MidiMessage) -> Option<Vec<u8>>

引数: - channel: MIDIチャンネル(0-15) - midly::num::u4は「4ビット符号なし整数」型 - message: MIDIメッセージの種類

戻り値: - Option<Vec<u8>>: バイト列(生成できない場合はNone)

8.2 MIDIメッセージの構造

MIDIメッセージは通常2〜3バイトで構成されます:

[ステータスバイト] [データバイト1] [データバイト2(省略可)]

ステータスバイト:

上位4ビット: メッセージタイプ
下位4ビット: チャンネル

8.3 チャンネル情報の埋め込み

let ch = channel.as_int();
let status_base = ch & 0x0F;
  • channel.as_int(): u4型をu8型に変換
  • & 0x0F: 下位4ビットだけを取り出す(念のため)

8.4 各MIDIメッセージの変換

Note Off (音を止める)

MidiMessage::NoteOff { key, vel } => {
    Some(vec![0x80 | status_base, key.as_int(), vel.as_int()])
}

ビット演算 | (OR):

0x80 = 1000 0000
ch   = 0000 0011 (例: チャンネル3)
-----------------
     = 1000 0011 = 0x83

結果: [0x83, 60, 64] = チャンネル3でノート60をベロシティ64で停止

Note On (音を鳴らす)

MidiMessage::NoteOn { key, vel } => {
    Some(vec![0x90 | status_base, key.as_int(), vel.as_int()])
}
  • ステータス: 0x90
  • 例: [0x90, 60, 100] = チャンネル0でノート60(ド)をベロシティ100で発音

Controller (コントロールチェンジ)

MidiMessage::Controller { controller, value } => {
    Some(vec![0xB0 | status_base, controller.as_int(), value.as_int()])
}
  • 音量、パン、エクスプレッションなどを制御
  • 例: [0xB0, 7, 100] = チャンネル0のボリュームを100に

Pitch Bend (ピッチベンド)

MidiMessage::PitchBend { bend } => {
    let value = bend.as_int();
    let lsb = (value & 0x7F) as u8;
    let msb = ((value >> 7) & 0x7F) as u8;
    Some(vec![0xE0 | status_base, lsb, msb])
}

14ビット値の分解: - ピッチベンドは14ビットの精度(0-16383) - 下位7ビット(LSB)と上位7ビット(MSB)に分ける

value = 8192 (中央値)
  = 0010 0000 0000 0000 (2進数)

lsb = value & 0x7F = 0x00
msb = (value >> 7) & 0x7F = 0x40

結果: [0xE0, 0x00, 0x40]

第9回: 再生処理 (1/2)

9.1 関数のシグネチャ

fn play(
    mut events: Vec<PlaybackEvent>, 
    ppqn: u16, 
    mut conn: MidiOutputConnection
) -> Result<()>

引数: - mut events: イベントリスト(ソートするので可変) - ppqn: 4分音符あたりのtick数 - mut conn: MIDI出力接続(送信するので可変)

9.2 イベントのソート

events.sort_by(|a, b| a.tick.cmp(&b.tick).then(a.order.cmp(&b.order)));

段階的に理解しましょう:

  1. sort_by: カスタム比較関数でソート
  2. |a, b|: 2つの要素を比較するクロージャ
  3. a.tick.cmp(&b.tick): まずtickで比較
  4. .then(...): 同じ場合は次の条件で比較
  5. a.order.cmp(&b.order): order で比較

なぜorderが必要? 同じtickに複数のイベントがある場合、ファイルに出現した順序を保つためです。

9.3 初期化

let ticks_per_beat = ppqn as u64;
let mut current_tick = 0u64;
let mut micros_per_beat = 500_000u64; // default 120 BPM

デフォルトテンポ: - 500,000μs/拍 = 0.5秒/拍 - BPM = 60 / 0.5 = 120

9.4 時間計算の原理

MIDIのtickを実時間に変換する式:

実時間(μs) = tick数 × (μs/拍) ÷ (tick/拍)

例:

tick数 = 480
μs/拍 = 500,000 (120 BPM)
tick/拍 = 480 (PPQN)

実時間 = 480 × 500,000 ÷ 480 = 500,000μs = 0.5秒

第10回: 再生処理 (2/2)

10.1 メインループ

for ev in events {
    // 1. 待機時間の計算
    let delta_ticks = ev.tick.saturating_sub(current_tick);
    
    // 2. 待機
    if delta_ticks > 0 {
        let delta_micros = delta_ticks * micros_per_beat / ticks_per_beat;
        if delta_micros > 0 {
            thread::sleep(Duration::from_micros(delta_micros));
        }
        current_tick = ev.tick;
    }
    
    // 3. イベント実行
    match ev.kind {
        EventKind::Midi(msg) => conn.send(&msg)?,
        EventKind::Tempo(new_tempo) => {
            micros_per_beat = new_tempo as u64;
        }
    }
}

10.2 待機時間の計算

let delta_ticks = ev.tick.saturating_sub(current_tick);

saturating_subとは? - 引き算だが、結果が負にならない(0で止まる) - 通常の-演算子だとオーバーフローの可能性

なぜこれが必要? 念のための安全策です。通常はソート済みなのでev.tick >= current_tickですが、万が一に備えています。

10.3 実時間への変換とスリープ

if delta_ticks > 0 {
    let delta_micros = delta_ticks * micros_per_beat / ticks_per_beat;
    if delta_micros > 0 {
        thread::sleep(Duration::from_micros(delta_micros));
    }
    current_tick = ev.tick;
}

計算例:

現在のtick: 0
次のイベント: 240 tick
PPQN: 480
テンポ: 500,000 μs/拍

delta_ticks = 240
delta_micros = 240 * 500,000 / 480 = 250,000 μs = 0.25秒

→ 0.25秒待つ

10.4 イベントの実行

match ev.kind {
    EventKind::Midi(msg) => {
        conn.send(&msg)?;
    }
    EventKind::Tempo(new_tempo) => {
        micros_per_beat = new_tempo as u64;
    }
}

MIDIメッセージ送信: - conn.send(&msg): バイト列をMIDI出力に送信 - ?: 送信エラーがあれば即座にreturn

テンポ変更: - micros_per_beatを更新 - 次のイベントからは新しいテンポで計算される


第11回: 実践演習とまとめ

11.1 プログラムの実行方法

1. プロジェクトの作成:

$ cargo new midi_player
$ cd midi_player

2. Cargo.tomlの編集:

[dependencies]
anyhow = "1.0"
midir = "0.9"
midly = "0.5"

3. src/main.rsにコードを貼り付け

4. 実行:

$ cargo run your_song.mid
$ cargo run your_song.mid 1  # ポート1を使用

11.2 このプログラムから学べる重要な概念

Rustの型システム

  • enum: 複数の選択肢を表現
  • struct: 複数のデータをまとめる
  • Option<T>: 値があるかないか
  • Result<T, E>: 成功か失敗か

エラー処理

  • ?演算子: エラーの伝播
  • anyhowクレート: 便利なエラー処理
  • context: エラーにコンテキストを追加

イテレータ

  • .iter(), .enumerate(), .map(), .collect()
  • メソッドチェーンで処理を記述

所有権と借用

  • &[u8]: 借用(参照)
  • mut: 可変性
  • 関数にデータを渡すときの所有権

11.3 改良のアイデア

皆さんが自分でプログラムを拡張するアイデア:

1. 機能追加:

// 再生速度の調整
fn play_with_speed(events: Vec<PlaybackEvent>, ppqn: u16, 
                   conn: MidiOutputConnection, speed: f64) -> Result<()> {
    // delta_microsにspeedを掛ける
    let adjusted_micros = (delta_micros as f64 / speed) as u64;
    thread::sleep(Duration::from_micros(adjusted_micros));
}

2. 再生位置の表示:

// 現在の再生位置を表示
let progress = (current_tick as f64 / total_ticks as f64) * 100.0;
println!("Progress: {:.1}%", progress);

3. 特定のトラックだけ再生:

// トラック番号をフィルタリング
fn load_events_filtered(data: &[u8], track_num: usize) -> Result<(Vec<PlaybackEvent>, u16)> {
    for (i, track) in smf.tracks.iter().enumerate() {
        if i != track_num {
            continue;
        }
        // 処理...
    }
}

4. 一時停止機能:

// キーボード入力で一時停止
use std::io::{self, Read};
use std::sync::mpsc;

// 別スレッドでキー入力を監視
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
    let mut buffer = [0u8; 1];
    loop {
        io::stdin().read(&mut buffer).unwrap();
        tx.send(()).unwrap();
    }
});

// メインループ内
if rx.try_recv().is_ok() {
    println!("Paused. Press any key to continue...");
    let mut buffer = [0u8; 1];
    io::stdin().read(&mut buffer).unwrap();
}

11.4 よくある問題と対処法

Q1: "No MIDI output ports found"と表示される - 原因: MIDI出力デバイスがない - 対処: - Windows: Microsoft GS Wavetable Synthが利用可能 - macOS: IAC Driverを有効化 - Linux: Timidity++やFluidSynthをインストール

Q2: 音が鳴らない - MIDI音源の設定を確認 - ポート番号を変えてみる: cargo run song.mid 1

Q3: コンパイルエラー - 依存関係のバージョンを確認 - cargo updateを実行

11.5 さらに学ぶために

関連トピック: 1. MIDIプロトコル: より深く理解するために 2. リアルタイム処理: より正確なタイミング制御 3. GUI: eguiクレートでGUIを追加 4. マルチスレッド: 再生と制御を分離

参考資料: - MIDIの仕様: https://www.midi.org/ - Rustの公式ドキュメント: https://doc.rust-lang.org/ - midirのドキュメント - midlyのドキュメント


まとめ

このMIDIプレイヤーを通じて、以下を学びました:

Rustの基本文法: enum, struct, match, if let
エラー処理: Result, Option, ?演算子
イテレータ: map, filter, collect
外部クレートの使用: Cargo.toml, use文
ファイルI/O: fs::read
時間制御: thread::sleep, Duration
バイナリデータ: ビット演算、バイト列の操作

このコードを理解できれば、中級レベルのRustプログラミングができるようになっています。

さあ、自分でも音楽プレイヤーや他のプログラムを作ってみましょう!

何か質問があれば、いつでも聞いてください。




以上の内容はhttps://memorandums.hatenablog.com/entry/2026/01/15/110339より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14