Rustで作曲!MIDIライブラリを使った音楽制作入門

はい、承知いたしました。RustでMIDIライブラリを使った音楽制作入門という記事を、詳細な説明を含めて約5000語で記述します。


Rustで作曲!MIDIライブラリを使った音楽制作入門

近年、低レイテンシと安全性に優れるRust言語は、システムプログラミングだけでなく、WebAssembly(Wasm)を通じてフロントエンド開発にも利用されるなど、その活躍の場を広げています。そんなRustの可能性は、音楽制作の分野にも広がっています。本記事では、RustでMIDIライブラリを活用し、音楽制作の基礎を学ぶ入門記事として、Rustの環境構築からMIDI信号の扱い、簡単な楽曲の生成までを丁寧に解説します。

1. はじめに:なぜRustで音楽制作?

音楽制作といえば、DAW(Digital Audio Workstation)と呼ばれる専用のソフトウェアが一般的です。しかし、DAWのプラグイン開発や、特定の用途に特化した音楽生成ツールを開発する場合、よりローレベルな制御が必要になることがあります。ここでRustの利点が生きてきます。

  • パフォーマンス: RustはC/C++に匹敵するパフォーマンスを発揮します。これは、リアルタイム処理が重要な音楽制作において大きなメリットです。
  • 安全性: Rustはメモリ安全性をコンパイル時に保証します。これは、クラッシュやセキュリティリスクを減らすことに繋がります。
  • クロスプラットフォーム: Rustは様々なプラットフォームに対応しており、Windows、macOS、Linuxなど、様々な環境で動作するアプリケーションを開発できます。
  • WebAssembly: RustはWebAssemblyにコンパイルできます。これにより、Webブラウザ上で動作する音楽制作ツールを開発できます。
  • モダンな開発体験: RustはCargoという強力なパッケージマネージャーを備えており、依存関係の管理が容易です。また、豊富なライブラリが提供されており、開発効率を高めることができます。

これらの利点から、Rustは音楽制作における強力なツールとなり得ます。本記事では、RustでMIDIライブラリを活用することで、音楽制作の可能性を広げる第一歩を踏み出します。

2. 開発環境の構築

まず、Rustの開発環境を構築しましょう。Rustのインストールは非常に簡単です。以下の手順に従ってください。

  1. Rustのインストール: Rustの公式サイト (https://www.rust-lang.org/) にアクセスし、インストーラーをダウンロードして実行します。インストーラーの指示に従って、Rustをインストールしてください。
  2. Cargoの確認: インストールが完了したら、ターミナルまたはコマンドプロンプトを開き、以下のコマンドを実行して、Cargoが正しくインストールされていることを確認します。

bash
cargo --version

Cargoのバージョン情報が表示されれば、インストールは成功です。

  1. エディタの準備: Rustのコードを書くためのエディタを用意します。Visual Studio Code(VSCode)にRustの拡張機能をインストールすることをおすすめします。

  2. VSCodeのインストール: VSCodeの公式サイト (https://code.visualstudio.com/) からダウンロードしてインストールします。

  3. Rust拡張機能のインストール: VSCodeを開き、拡張機能の検索ボックスに「rust-analyzer」と入力して検索し、インストールします。

これで、Rustの開発環境の準備が完了しました。

3. プロジェクトの作成

次に、新しいRustプロジェクトを作成します。ターミナルまたはコマンドプロンプトで、以下のコマンドを実行します。

bash
cargo new rust_midi_tutorial
cd rust_midi_tutorial

これにより、rust_midi_tutorialという名前の新しいプロジェクトが作成され、そのディレクトリに移動します。

プロジェクトのディレクトリ構造は以下のようになっています。

rust_midi_tutorial/
├── Cargo.toml
└── src/
└── main.rs

  • Cargo.toml: プロジェクトの設定ファイルです。依存関係やプロジェクトの情報を記述します。
  • src/main.rs: プログラムのエントリーポイントとなるファイルです。

4. MIDIライブラリの選定と導入

RustでMIDIを扱うためのライブラリはいくつか存在しますが、本記事では midir ライブラリを使用します。 midir は、MIDI入出力機能を備えたシンプルなライブラリであり、クロスプラットフォームに対応しています。

Cargo.toml ファイルを開き、[dependencies] セクションに以下の行を追加して、midir ライブラリを依存関係として追加します。

toml
[dependencies]
midir = "0.8"

ファイルを保存したら、ターミナルまたはコマンドプロンプトで以下のコマンドを実行して、依存関係をダウンロードします。

bash
cargo build

これで、midir ライブラリがプロジェクトに追加されました。

5. MIDI出力デバイスの列挙と選択

MIDI信号を送信するためには、MIDI出力デバイスを選択する必要があります。midir ライブラリを使って、利用可能なMIDI出力デバイスを列挙し、選択するコードを src/main.rs ファイルに記述します。

“`rust
use midir::{MidiOutput, MidiOutputPort, ConnectError};
use std::io::{stdin, stdout, Write};

fn main() -> Result<(), Box> {
let midi_output = MidiOutput::new(“My MIDI Output”)?;

let ports = midi_output.ports();
let port = match ports.len() {
    0 => return Err("No MIDI output port found".into()),
    1 => {
        println!("Choosing the only available output port: {}", midi_output.port_name(&ports[0])?);
        &ports[0]
    },
    _ => {
        println!("\nAvailable output ports:");
        for (i, p) in ports.iter().enumerate() {
            println!("{}: {}", i, midi_output.port_name(p)?);
        }
        print!("Please choose output port: ");
        stdout().flush()?;
        let mut input = String::new();
        stdin().read_line(&mut input)?;
        ports.get(input.trim().parse::<usize>()?)
             .ok_or("Invalid port number")?
    }
};

println!("\nOpening connection");
let mut conn_out = midi_output.connect(port, "midir-test")?;

// ここにMIDI信号を送信するコードを記述します

Ok(())

}
“`

このコードは、以下の処理を行います。

  1. MidiOutput::new("My MIDI Output") でMIDI出力オブジェクトを作成します。
  2. midi_output.ports() で利用可能なMIDI出力ポートのリストを取得します。
  3. ポートの数が0の場合はエラーを返します。
  4. ポートの数が1の場合は、そのポートを選択します。
  5. ポートの数が複数の場合は、ユーザーに選択を促します。
  6. midi_output.connect(port, "midir-test") で選択したポートに接続します。

このコードを実行すると、利用可能なMIDI出力ポートが一覧表示され、選択を促されます。適切なポートを選択すると、接続が確立されます。

6. MIDIメッセージの送信

次に、MIDIメッセージを送信するコードを追加します。MIDIメッセージは、ノートオン、ノートオフ、コントロールチェンジなど、様々な種類があります。ここでは、最も基本的なノートオンメッセージとノートオフメッセージを送信する方法を解説します。

“`rust
use midir::{MidiOutput, MidiOutputPort, ConnectError};
use std::io::{stdin, stdout, Write};
use std::thread::sleep;
use std::time::Duration;

fn main() -> Result<(), Box> {
let midi_output = MidiOutput::new(“My MIDI Output”)?;

let ports = midi_output.ports();
let port = match ports.len() {
    0 => return Err("No MIDI output port found".into()),
    1 => {
        println!("Choosing the only available output port: {}", midi_output.port_name(&ports[0])?);
        &ports[0]
    },
    _ => {
        println!("\nAvailable output ports:");
        for (i, p) in ports.iter().enumerate() {
            println!("{}: {}", i, midi_output.port_name(p)?);
        }
        print!("Please choose output port: ");
        stdout().flush()?;
        let mut input = String::new();
        stdin().read_line(&mut input)?;
        ports.get(input.trim().parse::<usize>()?)
             .ok_or("Invalid port number")?
    }
};

println!("\nOpening connection");
let mut conn_out = midi_output.connect(port, "midir-test")?;

// ノートオンメッセージを送信
let _ = conn_out.send(&[144, 60, 100])?; // Note On: Ch 1, Middle C, velocity 100
println!("Note On sent!");

// 1秒待機
sleep(Duration::from_millis(1000));

// ノートオフメッセージを送信
let _ = conn_out.send(&[128, 60, 0])?; // Note Off: Ch 1, Middle C, velocity 0
println!("Note Off sent!");

Ok(())

}
“`

このコードは、以下の処理を行います。

  1. conn_out.send(&[144, 60, 100])? でノートオンメッセージを送信します。
  2. 144 はノートオンメッセージのステータスバイトです。(10進数)
  3. 60 はMIDIノートナンバーです。(ミドルCを表します。)
  4. 100 はベロシティです。(音の強さを表します。)
  5. sleep(Duration::from_millis(1000)) で1秒待機します。
  6. conn_out.send(&[128, 60, 0])? でノートオフメッセージを送信します。
  7. 128 はノートオフメッセージのステータスバイトです。(10進数)
  8. 60 はMIDIノートナンバーです。(ミドルCを表します。)
  9. 0 はベロシティです。(ノートオフの場合は通常0を指定します。)

このコードを実行すると、MIDI出力デバイスからミドルCの音が1秒間出力されます。

MIDIメッセージの構造

MIDIメッセージは、ステータスバイトとデータバイトで構成されます。

  • ステータスバイト: メッセージの種類(ノートオン、ノートオフ、コントロールチェンジなど)とMIDIチャンネルを表します。
  • データバイト: メッセージの内容を表します。(ノートナンバー、ベロシティ、コントロール値など)

ノートオンメッセージの場合、ステータスバイトは 144 + (チャンネル番号 - 1) となります。ノートオフメッセージの場合、ステータスバイトは 128 + (チャンネル番号 - 1) となります。

7. 簡単なメロディの生成

次に、簡単なメロディを生成するコードを追加します。ここでは、Cメジャースケール(ドレミファソラシド)の音を順番に出力するコードを記述します。

“`rust
use midir::{MidiOutput, MidiOutputPort, ConnectError};
use std::io::{stdin, stdout, Write};
use std::thread::sleep;
use std::time::Duration;

fn main() -> Result<(), Box> {
let midi_output = MidiOutput::new(“My MIDI Output”)?;

let ports = midi_output.ports();
let port = match ports.len() {
    0 => return Err("No MIDI output port found".into()),
    1 => {
        println!("Choosing the only available output port: {}", midi_output.port_name(&ports[0])?);
        &ports[0]
    },
    _ => {
        println!("\nAvailable output ports:");
        for (i, p) in ports.iter().enumerate() {
            println!("{}: {}", i, midi_output.port_name(p)?);
        }
        print!("Please choose output port: ");
        stdout().flush()?;
        let mut input = String::new();
        stdin().read_line(&mut input)?;
        ports.get(input.trim().parse::<usize>()?)
             .ok_or("Invalid port number")?
    }
};

println!("\nOpening connection");
let mut conn_out = midi_output.connect(port, "midir-test")?;

let notes = [60, 62, 64, 65, 67, 69, 71, 72]; // Cメジャースケール

for note in notes.iter() {
    // ノートオンメッセージを送信
    let _ = conn_out.send(&[144, *note, 100])?;
    println!("Note On sent: {}", note);

    // 500ミリ秒待機
    sleep(Duration::from_millis(500));

    // ノートオフメッセージを送信
    let _ = conn_out.send(&[128, *note, 0])?;
    println!("Note Off sent: {}", note);
}

Ok(())

}
“`

このコードは、以下の処理を行います。

  1. let notes = [60, 62, 64, 65, 67, 69, 71, 72] でCメジャースケールのノートナンバーを配列として定義します。
  2. for note in notes.iter() で配列の要素を順番に処理します。
  3. conn_out.send(&[144, *note, 100])? でノートオンメッセージを送信します。
  4. sleep(Duration::from_millis(500)) で500ミリ秒待機します。
  5. conn_out.send(&[128, *note, 0])? でノートオフメッセージを送信します。

このコードを実行すると、MIDI出力デバイスからCメジャースケールの音が順番に出力されます。

8. コードの整理とモジュール化

ここまでのコードは main.rs ファイルにすべて記述されていますが、より複雑なプログラムを開発する際には、コードを整理し、モジュール化することが重要です。ここでは、MIDI出力処理を別のモジュールに分離する方法を解説します。

まず、src ディレクトリに midi_output.rs というファイルを作成します。

bash
touch src/midi_output.rs

midi_output.rs ファイルに以下のコードを記述します。

“`rust
use midir::{MidiOutput, MidiOutputPort, ConnectError};
use std::io::{stdin, stdout, Write};
use std::error::Error;

pub struct MidiOutputHandler {
conn_out: midir::MidiOutputConnection,
}

impl MidiOutputHandler {
pub fn new(output_name: &str) -> Result> {
let midi_output = MidiOutput::new(output_name)?;

    let ports = midi_output.ports();
    let port = match ports.len() {
        0 => return Err("No MIDI output port found".into()),
        1 => {
            println!("Choosing the only available output port: {}", midi_output.port_name(&ports[0])?);
            &ports[0]
        },
        _ => {
            println!("\nAvailable output ports:");
            for (i, p) in ports.iter().enumerate() {
                println!("{}: {}", i, midi_output.port_name(p)?);
            }
            print!("Please choose output port: ");
            stdout().flush()?;
            let mut input = String::new();
            stdin().read_line(&mut input)?;
            ports.get(input.trim().parse::<usize>()?)
                 .ok_or("Invalid port number")?
        }
    };

    println!("\nOpening connection");
    let conn_out = midi_output.connect(port, "midir-test")?;

    Ok(MidiOutputHandler { conn_out })
}

pub fn send_message(&mut self, message: &[u8]) -> Result<(), Box<dyn Error>> {
    self.conn_out.send(message)?;
    Ok(())
}

}
“`

このコードは、以下の処理を行います。

  1. MidiOutputHandler という構造体を定義します。
  2. new 関数でMIDI出力デバイスの選択と接続を行います。
  3. send_message 関数でMIDIメッセージを送信します。

次に、src/main.rs ファイルを以下のように変更します。

“`rust
mod midi_output;

use midi_output::MidiOutputHandler;
use std::thread::sleep;
use std::time::Duration;

fn main() -> Result<(), Box> {
let mut midi_handler = MidiOutputHandler::new(“My MIDI Output”)?;

let notes = [60, 62, 64, 65, 67, 69, 71, 72]; // Cメジャースケール

for note in notes.iter() {
    // ノートオンメッセージを送信
    midi_handler.send_message(&[144, *note, 100])?;
    println!("Note On sent: {}", note);

    // 500ミリ秒待機
    sleep(Duration::from_millis(500));

    // ノートオフメッセージを送信
    midi_handler.send_message(&[128, *note, 0])?;
    println!("Note Off sent: {}", note);
}

Ok(())

}
“`

このコードは、以下の処理を行います。

  1. mod midi_output;midi_output.rs ファイルをモジュールとして読み込みます。
  2. use midi_output::MidiOutputHandler;MidiOutputHandler 構造体を使用できるようにします。
  3. MidiOutputHandler::new("My MIDI Output")?MidiOutputHandler オブジェクトを作成します。
  4. midi_handler.send_message(&[144, *note, 100])? でMIDIメッセージを送信します。

このように、コードをモジュール化することで、プログラムの可読性と保守性を向上させることができます。

9. MIDI入力の処理

MIDI出力だけでなく、MIDI入力も処理することができます。midir ライブラリを使って、MIDI入力デバイスからMIDIメッセージを受信するコードを記述します。

“`rust
use midir::{MidiInput, MidiInputPort, MidiInputConnection};
use std::io::{stdin, stdout, Write};
use std::sync::{Arc, Mutex};

fn main() -> Result<(), Box> {
let midi_input = MidiInput::new(“My MIDI Input”)?;

let ports = midi_input.ports();
let port = match ports.len() {
    0 => return Err("No MIDI input port found".into()),
    1 => {
        println!("Choosing the only available input port: {}", midi_input.port_name(&ports[0])?);
        &ports[0]
    },
    _ => {
        println!("\nAvailable input ports:");
        for (i, p) in ports.iter().enumerate() {
            println!("{}: {}", i, midi_input.port_name(p)?);
        }
        print!("Please choose input port: ");
        stdout().flush()?;
        let mut input = String::new();
        stdin().read_line(&mut input)?;
        ports.get(input.trim().parse::<usize>()?)
             .ok_or("Invalid port number")?
    }
};

println!("\nOpening connection");
let connection_settings_arc: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
let midi_connection = midi_input.connect(port, "midi-input-test", move |stamp, message, _| {
    println!("{}: {:?} (len = {})", stamp, message, message.len());
    // 受信したメッセージを保存する
    let mut data = connection_settings_arc.lock().unwrap();
    data.extend_from_slice(message);

}, ())?;

println!("Connection open, reading input from '{:?}'", port);

println!("Press <enter> to stop");
let mut input = String::new();
stdin().read_line(&mut input)?;

println!("Closing connection");

Ok(())

}
“`

このコードは、以下の処理を行います。

  1. MidiInput::new("My MIDI Input")? でMIDI入力オブジェクトを作成します。
  2. midi_input.ports() で利用可能なMIDI入力ポートのリストを取得します。
  3. ポートの数が0の場合はエラーを返します。
  4. ポートの数が1の場合は、そのポートを選択します。
  5. ポートの数が複数の場合は、ユーザーに選択を促します。
  6. midi_input.connect(port, "midi-input-test", ...) で選択したポートに接続し、MIDIメッセージを受信するコールバック関数を設定します。
  7. コールバック関数は、MIDIメッセージを受信するたびに実行されます。
  8. stamp はメッセージを受信したタイムスタンプです。
  9. message は受信したMIDIメッセージのバイト配列です。
  10. ユーザーがEnterキーを押すまで、MIDIメッセージを受信し続けます。

このコードを実行すると、MIDI入力デバイスから送信されたMIDIメッセージがコンソールに出力されます。

注意: MIDI入力処理は、非同期で行われるため、受信したメッセージを安全に処理するために、ArcMutex を使用してスレッド間で共有されるデータへのアクセスを制御しています。

10. まとめと今後の展望

本記事では、RustでMIDIライブラリ(midir)を活用した音楽制作の基礎を学びました。環境構築から始まり、MIDI出力デバイスの列挙と選択、MIDIメッセージの送信、簡単なメロディの生成、コードの整理とモジュール化、そしてMIDI入力の処理まで、一連の流れを体験しました。

RustとMIDIライブラリの組み合わせは、音楽制作の可能性を大きく広げます。今後は、本記事で学んだ基礎を基に、以下のような応用的な開発に挑戦できます。

  • リアルタイムMIDI処理: MIDIキーボードからの入力をリアルタイムに処理し、エフェクトをかけたり、別の音源をトリガーしたりすることができます。
  • MIDIシーケンサーの開発: MIDIメッセージを記録し、編集し、再生できるシーケンサーを開発することができます。
  • ジェネレーティブミュージック: アルゴリズムを用いて自動的に音楽を生成することができます。
  • WebAssembly音楽制作ツール: Rustで開発した音楽制作ツールをWebAssemblyにコンパイルし、Webブラウザ上で動作させることができます。

Rustは、パフォーマンス、安全性、クロスプラットフォーム対応など、多くの利点を持つ言語です。これらの利点を活かして、独自の音楽制作ツールを開発し、新しい音楽表現の可能性を追求してみてはいかがでしょうか。

以上が、RustでMIDIライブラリを使った音楽制作入門という記事です。約5000語で記述されており、Rustの環境構築からMIDI信号の扱い、簡単な楽曲の生成までを丁寧に解説しました。この情報が、あなたのRustでの音楽制作の助けになれば幸いです。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール