前回、RustySynthクレートを使って、サウンドフォント対応のシンセを作る方法を紹介しました。今回は、MIDIデータとサウンドフォントを読み込んで演奏を行うMIDIプレイヤーを作ってみましょう。
MIDIファイルとWAVファイルの違い
前回、WAVファイルの仕組みや音声データについて紹介しました。それから、サウンドフォントを利用して波形を書き出す方法も紹介しました。今回は、演奏データのMIDIファイルを読み込んで、演奏するプログラムを作ります。
そもそも、MIDIファイルとは電子楽器で音楽を演奏するための演奏データです。そのため、MIDIファイルを再生する際にシンセを変更すると異なる音色で音楽を奏でることができます。
WAVファイルは、どのプレイヤーで再生しても全く同じ音色が演奏されますが、MIDIファイルはどのシンセを使って演奏するかによって異なる音色になるのです。
今回作成するMIDIプレイヤーは、シンセの部分がサウンドフォントに相当します。インターネット上でさまざまなサウンドフォントが配布されているため、これを差し替えることで、いろいろな音色で再生することができます。
サウンドフォントでMIDIデータをWAVファイルに書き出す
さて、前回、RustySynthの基本的な使い方を紹介しました。今回は、MIDIデータを読み込んで、WAVファイルを書き出してみましょう。と言っても、実は、RustySynthクレートにMIDIデータを読み込む機能が備わっています。ここでは、その機能を利用してWAVファイルを書き出すプログラムを作ってみましょう。
まずは、プロジェクトを作成しましょう。ターミナルで以下のコマンドを実行しましょう。
# プロジェクトを初期化
$ cargo init
# 必要なクレートを追加
$ cargo add rustysynth@1.2.0
$ cargo add wav_io@0.1.8
すると、プロジェクトのひな形が作成されます。
続いて、前回で利用したサウンドフォントの音源ファイル「TimGM6mb.sf2」を用意しましょう。また、サンプルのMIDIファイル「sakura2.mid」をこちらからダウンロードしましょう。
そして、下記のようにディレクトリに配置します。
.
├── Cargo.toml --- 設定ファイル
├── TimGM6mb.sf2 --- サウンドフォントのファイル
├── sakura2.mid --- 「さくらさくら」のMIDIファイル
└── src
└── main.rs --- メインファイル
これらのファイルは次のように利用されます。MIDIファイル「sakura2.mid」と音源データであるサウンドフォント「TimGM6mb.sf2」を読み込んで、WAVファイル「sakura2.wav」を書き出します。
それでは、プログラムを作りましょう。src/main.rsを開いて以下のプログラムに書き換えましょう。プログラム全体は、こちらにもアップロードしています。
use rustysynth::{SynthesizerSettings, Synthesizer, SoundFont, MidiFile, MidiFileSequencer};
use wav_io;
// 定数の指定 --- (*1)
const FILE_SOUNDFONT: &str = "TimGM6mb.sf2"; // サウンドフォントのパス
const FILE_MIDI: &str = "sakura2.mid"; // 入力MIDIファイルのパス
const FILE_WAV: &str = "sakura2.wav"; // 出力WAVファイルのパス
const SAMPLE_RATE: u32 = 44_100; // サンプリング周波数(CD音質)
fn main() {
// 用意したサウンドフォントを読み込む --- (*2)
let mut sf2 = std::fs::File::open(FILE_SOUNDFONT).unwrap();
let sound_font = std::sync::Arc::new(SoundFont::new(&mut sf2).unwrap());
// シンセサイザーの作成 --- (*3)
let settings = SynthesizerSettings::new(SAMPLE_RATE as i32);
let synthesizer = Synthesizer::new(&sound_font, &settings).unwrap();
// MIDIシーケンサーの作成 --- (*4)
let mut sequencer = MidiFileSequencer::new(synthesizer);
// MIDIファイルの読み込み --- (*5)
let mut mid = std::fs::File::open(FILE_MIDI).unwrap();
let midi_file = std::sync::Arc::new(MidiFile::new(&mut mid).unwrap());
let midi_time_len = midi_file.get_length();
// MIDIデータをオーディオ化するのに必要なサンプル数を計算 --- (*6)
let sample_count = (SAMPLE_RATE as f64 * midi_time_len) as usize;
// 書き込み先のバッファを確保 --- (*7)
let mut samples = vec![0.0f32; sample_count * 2];
let mut left_buf = vec![0.0f32; sample_count];
let mut right_buf = vec![0.0f32; sample_count];
// サウンドフォントを書き込み --- (*8)
sequencer.play(&midi_file, false);
sequencer.render(&mut left_buf[..], &mut right_buf[..]);
for i in 0..left_buf.len() {
samples[i*2+0] = left_buf[i];
samples[i*2+1] = right_buf[i];
}
// WAVファイルへ保存 --- (*9)
let mut wav_head = wav_io::new_stereo_header();
wav_head.sample_rate = SAMPLE_RATE;
let mut wav_out = std::fs::File::create(FILE_WAV).unwrap();
wav_io::write_to_file(&mut wav_out, &wav_head, &samples).unwrap();
}
プログラムを実行してみましょう。ターミナルに以下のコマンドを入力します。すると「sakura2.wav」というWAVファイルが作成されます。作成されたWAVファイルをメディアプレイヤーなどで演奏してみてください。すると日本民謡の「さくらさくら」が演奏されます。
$ cargo run
前回紹介した波形編集ツールのAudacityを使うと書き出されたWAVファイルの波形を詳しく確認できます。
プログラムを確認してみましょう。(*1)では、サウンドフォントのファイル名や、入力MIDIファイルのパス、出力先のWAVファイルのパスなどを指定します。
(*2)ではRustySynthを利用してサウンドフォントを読み出します。(*3)ではシンセサイザーを作成します。
(*4)ではMIDIシーケンサーを作成し、(*5)ではMIDIファイルを読み込みます。
(*6)では、MIDIデータをオーディオデータとして書き込むのに必要なサンプル数を計算します。
そして、(*7)の部分で書き込み用のバッファを確保します。(*8)でサウンドフォントを利用して波形データを作成し、Vec
そして、(*9)でWAVファイルへデータを書き出します。
ファイルに書き出さず直接再生したい場合
次に、WAVファイルに書き出すのではなく、音声データを再生するプレイヤーを作成してみましょう。
上記のプログラムを実行すると分かりますが、WAVファイルへ音声データを書き出すのはそれなりに時間がかかってしまいます。しかし、作成した波形データをOSのオーディオデバイスに直接書き込むようにするなら、待ち時間なく演奏を始めることができます。
そのために、tinyaudioクレートを使います。このクレートは、Windows/Linux/macOS/WebAssembly/Android/iOSとメジャーなOSに対応しており、端末のオーディオデバイスにデータを直接送信することができます。なお、WSL環境で動かすためには、PulseAudioのインストールなどが必要になってしまうため、WindowsのネイティブRustで試してください。
それでは、プロジェクトを作ってみましょう。
# プロジェクトを初期化
$ cargo init
# 必要なクレートを追加
$ cargo add rustysynth@1.2.0
$ cargo add tinyaudio@0.1.1
そして、先ほどと同様に、サウンドフォントの「TimGM6mb.sf2」とMIDIファイル「sakura2.mid」を下記のようにディレクトリに配置します。
.
├── Cargo.toml --- 設定ファイル
├── TimGM6mb.sf2 --- サウンドフォントのファイル
├── sakura2.mid --- 「さくらさくら」のMIDIファイル
└── src
└── main.rs --- メインファイル
次に、メインファイルである「src/main.rs」を下記のように書き換えましょう。これは、WAVファイルの書き出しをせず、オーディオデバイスに対して直接音声データを書き込むプログラムです。こちらにも、プログラムをアップロードしています。
use rustysynth::{SynthesizerSettings, Synthesizer, SoundFont, MidiFile, MidiFileSequencer};
use tinyaudio::prelude::*;
// 定数の指定 --- (*1)
const FILE_SOUNDFONT: &str = "TimGM6mb.sf2"; // サウンドフォントのパス
const FILE_MIDI: &str = "sakura2.mid"; // 入力MIDIファイルのパス
const SAMPLE_RATE: usize = 44_100; // サンプリング周波数(CD音質)
fn main() {
// オーディオの出力設定 --- (*2)
let params = OutputDeviceParameters {
channels_count: 2, // ステレオ
sample_rate: SAMPLE_RATE, // サンプリング周波数
channel_sample_count: SAMPLE_RATE / 2, // バッファサイズ
};
// 書き込み先のバッファを確保 --- (*3)
let mut left_buf:Vec<f32> = vec![0.0f32; params.channel_sample_count];
let mut right_buf:Vec<f32> = vec![0.0f32; params.channel_sample_count];
// 用意したサウンドフォントを読み込む --- (*4)
let mut sf2 = std::fs::File::open(FILE_SOUNDFONT).unwrap();
let sound_font = std::sync::Arc::new(SoundFont::new(&mut sf2).unwrap());
// シンセサイザーの作成 --- (*5)
let settings = SynthesizerSettings::new(SAMPLE_RATE as i32);
let synthesizer = Synthesizer::new(&sound_font, &settings).unwrap();
// MIDIシーケンサーを作成してMIDIファイルを読み込む --- (*6)
let mut sequencer = MidiFileSequencer::new(synthesizer);
let mut mid = std::fs::File::open(FILE_MIDI).unwrap();
let midi_file = std::sync::Arc::new(MidiFile::new(&mut mid).unwrap());
// シーケンサーの開始 --- (*7)
sequencer.play(&midi_file, true); // 繰り返し再生を有効にする
// オーディオの出力を開始 --- (*8)
let _device = run_output_device(params, {
move |data| {
// 再生位置を標準出力に表示
println!("{:03.1}/{} [Enter]で終了", sequencer.get_position(),
midi_file.get_length() as u32);
// シーケンサーによる波形生成 --- (*9)
let mut clock = 0;
sequencer.render(&mut left_buf[..], &mut right_buf[..]);
// オーディオデバイスに書き込む --- (*10)
for samples in data.chunks_mut(params.channels_count as usize) {
// チャンネルごとに波形を書き込む --- (*11)
for (ch, sample) in samples.iter_mut().enumerate() {
let v = if ch == 0 { left_buf[clock] } else { right_buf[clock] };
*sample = v;
}
clock = (clock + 1) % params.channel_sample_count;
}
}
})
.unwrap();
// [Enter]で終了 --- (*12)
std::io::stdin().read_line(&mut String::new()).unwrap();
}
プログラムを実行するには、ターミナルで次のコマンドを実行します。
$ cargo run
正しくプログラムが実行されると、MIDIファイルを読み込んで「さくらさくら」の演奏が始まります。ターミナルで[Enter]キーを押すと演奏が中止されプログラムが終了します。
プログラムを確認してみましょう。(*1)では定数の指定を行います。