※この記事は文系的冗長性及び反知性的曖昧主義に支配されています1。
現代楽譜学概論あるいは錆びついた頭にオイルを
Null安全2が一部界隈を賑わせた2016年11月。貴重な休日を湯水のように費やして不毛な作業に明け暮れる文系日曜プログラマーである私は、案の定、天啓を得ました。蘇る記憶。30億のデバイス。ヌルポ。ガッ。もういやだ、モダンなラングエッヂをライトしたい……!3
とはいえ、限られた時間で書く以上、目的をもって取り組みたいというもの。一通り思いを巡らせて浮かんだのが、C++を覚えるためにSMFパーサーを書いた経験。ビット演算からファイル入出力まで一通り必要となる上に、規模感が手頃で、新たな言語を覚えるのにそこそこ向いています。何よりちょうど、より高機能なものに書き直したいと思っていました。というわけで、C++の代替言語(と目される)RustでSMFパーサーライブラリghakuf
を書きました。この記事は、コードの解説と公開までの道のりを記すものです。
立てばPC、座ればDAW、歩く姿はSMF
ところでSMF(Standard MIDI File)をご存知ですか?MIDI4のファイルフォーマットの一つで、要するにデジタル音源に対応した音符と演奏記号の集合体(楽譜データ)です。「.mid」となっているファイルは十中八九これ。90年台後半に著作権を無視したSMFをやりとりする人が多発してしまったため、懐かしさを覚えるもといアンダーグラウンドな印象を受ける人がいるかもしれません。セスエムエフコワクナイ。ブンケイウソツカナイ。
SMFのファイル構造は以下のようなイメージ。バイナリデータ(≠プレーンテキスト)であることに注意。
SMF
├MThd :4byte ヘッダー
│├Length :4byte ヘッダーのサイズ。ヘッダーは形式が決まっているので必ず6(byte)。
│├SMF Type :2byte SMFのタイプ。普通は1。
│├Number Tracks:2byte トラックの数
│└Time Division:2byte 時間単位。演奏タイミングの計算に使用。通常は正数で、4分音符あたりの分解能が入る。
├MTrk :4byte 指揮者トラック(Conductor Track)
│├Length :VLQ トラックのサイズ
│├Meta Event メタ情報
│├Meta Event メタ情報
│├ ...
│└Meta Event :3byte トラックの終端を示すメタ情報(End of Track)
├MTrk :4byte トラック1
│├Length :VLQ トラックのサイズ
│├MIDI Event 演奏データ
│├MIDI Event 演奏データ
│├MIDI Event 演奏データ
│├MIDI Event 演奏データ
│├ ...
│└Meta Event :3byte トラックの終端を示すメタ情報(End of Track)
├MTrk :4byte トラック2
├ ...
基本的には、先頭にヘッダーがあって、その後にトラックと呼ばれるデータの塊が続きます。トラックには以下の3種類のデータのいずれかが格納されています。
イベント | 概要 | 代表例 |
---|---|---|
MIDIイベント | いわゆる演奏情報。ここで発声スタート!とか、ここで演奏方法を変更!とか。 | ノート・オン、ポリフォニックキープレッシャー、チャンネル・チェンジ |
メタ・イベント | 演奏情報以外のメタ情報。全体の演奏スピードや楽譜全体に適用される情報とか。 | テンポ、著作権情報、トラック終端 |
システム・エクスクルーシブ・イベント | 音源等に送信される特殊な情報。MIDIを通して機器間の通信を行う際に用いられる。 | (SMFで使われているところを見たことない……) |
このほか、可変長数値表現(VLQ)やランニングステータスが抑えておくべきポイントになりますが、以下のとおり良質な日本語の解説も多いため、詳細な仕様解説はそちらでご確認を。
- わいやぎ氏「SMF (Standard MIDI Files) の構造」(個人サイト、2010年10月)
- g200kg氏「DTM技術情報」(個人サイト、2011年9月)
- EternalWindows氏「サウンド / MIDI」(個人サイト、2006年)
ハッカーもすなるRustといふもの
流行りそうで流行らない少し流行っているイマドキ言語。そのコンセプトは、公式ガイドブックの頭書きに集約されています。
プログラミング言語Rust
Rustは安全性、速度、並行性の3つのゴールにフォーカスしたシステムプログラミング言語です。 ガーベジコレクタなしにこれらのゴールを実現していて、他の言語への埋め込み、要求された空間や時間内での動作、 デバイスドライバやオペレーティングシステムのような低レベルなコードなど他の言語が苦手とする多数のユースケースを得意とします。 全てのデータ競合を排除しつつも実行時オーバーヘッドのないコンパイル時の安全性検査を多数持ち、これらの領域をターゲットに置く既存の言語を改善します。 Rustは高級言語のような抽象化も含めた「ゼロコスト抽象化」も目標としています。 そうでありつつもなお低級言語のような精密な制御も許します。
この公式ガイドブックが非常に良くできていて、かつ最新の仕様がまとめられている5ので、これだけで初心者が十分に勉強できます。その他では、Rustの感触をつかむまで以下のサイトが大変役に立ちました。
- いもす氏「Rustは何が新しいのか(基本的な言語機能の紹介)」(個人サイト、2017年1月)
- Raphael ‘kena’ Poss氏「関数型プログラマのためのRust」(個人サイト、2014年7月、POSTD訳)
- Benjamin Fry氏「RustとDNSの1年」(個人ブログ、2016年8月、POSTD訳)
ちなみに、半年ほどRustを触って最も得心が行ったのは以下のツイート。
rust はシステムプログラミング云々を抜きにして、メモリ管理界隈の「もう人類はGC無しじゃ無理なんじゃ?」vs「根性という名の知性でやればできる」という対立構造に対して「コンパイラで延々と殴れば人は正しい道を歩ける」という第三極を提示したのが良かった気がする。何も知らんけど。
— はんぺんプログラマー (@hanpen_good) 2017年2月1日
Rustでプログラミングをするにあたって、エディターはAtomを採用しました。理由はとくにありませんが、私の開発環境がWindowsとMacなので、EmacsやVimより使いやすいかなーというくらい6。参考までに、使用しているパッケージは以下のとおり。
パッケージ名 | 作者 | 概要 |
---|---|---|
Atom Beautify | Glavin001氏 | コード整形を自動で行ってくれます。Rustにも対応。 |
Build Cargo | (Atom公式) | Rustのビルドシステム「Cargo」のコマンドをAtomに追加してくれます。「cargo doc --no-deps」も追加してほしい……。 |
Language Rust | zargony氏 | Rustのシンタックス・ハイライト(関数や変数といった意味に応じて色を変えるアレ)です。 |
Linter Rust | (Atom公式) | Rustの構文チェックをしてくれます。たまにうるさい。 |
Racer | edubkendo氏 | Rustのコード補完(コード入力中に、その続きの候補を出してくれるアレ)をしてくれます。事前にCargoからRacerをインストールしておく必要があります。 |
マクガフィンたるSMF、ときどきRustのオノマトペ
前置きが長くなりましたが、SMFパーサーをRustのライブラリとして実装するまでの道のりは以下のとおり。完成形だけ知りたい方はレポジトリまたはドキュメントへどうぞ。
おたまじゃくしは電子音符の夢を見るか?
前節で簡単に紹介したとおり、SMFは基本的に各トラックに記述されたMIDIイベント等からなるため、これをRustで表現します。
pub enum Message {
MetaEvent { delta_time: u32, event: MetaEvent, data: Vec<u8> },
MidiEvent { delta_time: u32, event: MidiEvent },
SysExEvent { delta_time: u32, event: SysExEvent, data: Vec<u8> },
TrackChange,
}
pub enum MetaEvent {
SequenceNumber,
TextEvent,
// 中略
Unknown { event_type: u8 },
}
pub enum MidiEvent {
NoteOff { ch: u8, note: u8, velocity: u8 },
NoteOn { ch: u8, note: u8, velocity: u8 },
// 中略
Unknown { ch: u8 },
}
pub enum SysExEvent {
// 中略
}
各イベントにはデルタタイムと呼ばれる時間情報が記録されており、また、可変長のデータが含まれることがあるため、まずは上位の列挙型7Message
で各イベントを記述し、それぞれの詳細を下位の列挙型(MetaEvent
、MIDIEvent
、SysExEvent
)で定義しています。パーサーがSMFでイベントを発見するたびにMessage
を作成します。
ちなみにTrackChange
というイベントはSMFに存在しませんが、後述のとおりパーサーの設計をイベント駆動型にするために定義しています。
イベントよみに与ふる書
今回は作成するのはライブラリなので、イベント駆動型8を採用し、Observerパターンで実装しています。Rustにクラスやプロトタイプは存在しないため、トレイトでオブザーバーが満たすべき境界を実装します。
pub trait Handler {
fn header(&mut self, format: u16, track: u16, time_base: u16) {
let _ = (format, track, time_base);
}
fn meta_event(&mut self, delta_time: u32, event: &MetaEvent, data: &Vec<u8>) {
let _ = (delta_time, event, data);
}
fn midi_event(&mut self, delta_time: u32, event: &MidiEvent) {
let _ = (delta_time, event);
}
fn sys_ex_event(&mut self, delta_time: u32, event: &SysExEvent, data: &Vec<u8>) {
let _ = (delta_time, event, data);
}
fn track_change(&mut self) {}
fn status(&mut self) -> HandlerStatus {
HandlerStatus::Continue
}
}
pub enum HandlerStatus {
Continue,
SkipTrack,
SkipAll,
}
パーサーがMIDIイベントを見つけるとfn midi_event(...)
で、メタ・イベントを見つけるとfn meta_event(...)
で、事前に登録されたオブザーバーに通知します。イベントを通知するだけではトラックの変わり目を伝えられないため、fn track_change(...)
も用意しています。
その他、これ以上のパースが不要になった際にファイルIOをスキップするため、fn status(...)
を通して、オブザーバーが状況を列挙型HandlerStatus
でパーサーに伝えられるようにしています。
観測者にラブソングを
それはいよいよパーサーの解説。
pub struct Reader {
file: io::BufReader<fs::File>,
handlers: Vec<Box<Handler>>,
path: path::PathBuf,
}
impl Reader {
pub fn new(handler: Box<Handler>, path: &str) -> Result<Reader, ReadError> {
let mut handlers: Vec<Box<Handler>> = Vec::new();
handlers.push(handler);
Ok(Reader {
file: io::BufReader::new(fs::OpenOptions::new().read(true).open(&path)?),
path: path::PathBuf::from(path),
handlers: handlers,
})
}
pub fn read(&mut self) -> Result<(), ReadError> {
// 中略:Handlerの状態チェック
self.file.seek(io::SeekFrom::Start(0))?;
self.check_tag(Tag::Header)?;
self.read_header_block()?;
while self.check_tag(Tag::Track)? {
// 中略:Handlerの状態チェック
self.read_track_block()?;
}
Ok(())
}
// 中略
}
Reader::new(...)
で観測者やSMFのファイルパスを登録するところからスタートします。Reader
のメンバーhandlers
がベクターであることからわかるとおり、観測者は複数登録できます9。準備ができればfn read(...)
でパース開始。ヘッダーを読み終えた後、MIDIイベント等を読むためのメインループに入ります。
ちなみに、Rustでファイル入出力を行う際はEOFチェックとバッファリングの扱いが直感(C言語)と異なるため、注意する必要があります。
- mkaito氏・Shepmaster氏「How to check for EOF with `read_line()`?」(stackoverflow、2014年12月)
- gyu-don氏「RustのファイルI/OにはBufReader, BufWriterを使いましょう、という話」(Qiita記事、2017年3月)
MIDIイベント等を読むコードは以下のとおり。トラック冒頭に記述されたデータサイズがつきるまで、発見したイベントを観測者に知らせ続けます。
fn read_track_block(&mut self) -> Result<&mut Reader, ReadError> {
let mut data_size = self.file.read_u32::<BigEndian>()?;
while data_size > 0 {
// 中略:Handlerの状態チェック
let delta_time = self.read_vlq()?;
data_size -= delta_time.len() as u32;
let mut status = self.file.read_u8()?;
// 中略:ランニングステータスの処理
data_size -= size_of::<u8>() as u32;
match status {
0xff => {
// meta eventの処理
let meta_event = MetaEvent::new(self.file.read_u8()?);
data_size -= size_of::<u8>() as u32;
let len = self.read_vlq()?;
let data = self.read_data(&len)?;
data_size -= len.len() as u32 + len.val();
for handler in &mut self.handlers {
if handler.status() == HandlerStatus::Continue {
handler.meta_event(delta_time.val(), &meta_event, &data);
}
}
}
0x80...0xef => {
// 中略:midi eventの処理
}
0xf0 | 0xf7 => {
// 中略:system exclusive eventの処理
}
_ => {
// 中略:想定外のイベントが発見された際のエラー処理
}
};
}
Ok(self)
}
SMFの各イベントは先頭のステータスバイトでその種類を判別するため、当該バイトをmatch
で振り分けて処理しています。ところで、SMFはビッグエンディアンで値が格納されているため、byteorderというクレートを使用しています。便利ですね。ちなみに、MetaEvent::new(...)
なんてしれっと使っていますが、これは、Rustでは列挙体にメソッドを実装できるためです。その他、バイナリを作成するメソッドfn binary(...)
等を独自に実装しています。
なお、今回はライブラリを作成しているため、エラー処理10を以下のとおり少し丁寧に書いています。
pub enum ReadError {
InvalidHeaderTag { tag: [u8; 4], path: path::PathBuf },
InvalidIdentifyCode { code: u32, path: path::PathBuf },
InvalidTrackTag { tag: [u8; 4], path: path::PathBuf },
Io(io::Error),
NoValidHandler,
UnknownMessageStatus { status: u8, path: path::PathBuf },
}
impl fmt::Display for ReadError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use reader::ReadError::*;
match *self {
InvalidHeaderTag { tag, ref path } => {
write!(
f,
"Invalid header tag '{:?}' has found: {}",
tag,
fs::canonicalize(&path).unwrap().display()
)
}
// 中略
}
}
}
impl error::Error for ReadError {
fn description(&self) -> &str {
use reader::ReadError::*;
match *self {
InvalidHeaderTag { .. } => "(エラー文)",
// 中略
}
}
}
impl From<io::Error> for ReadError {
fn from(err: io::Error) -> ReadError {
ReadError::Io(err)
}
}
パース時に生じうる各種エラーを列挙体ReadError
に掲げ、エラーを表現するerror::Error
トレイト及びデバッグ時の表示を定めるstd::fmt::Display
トレイトを実装しています。また、ReadError
のうちstd::io::Error
はstd
モジュールに定義されるエラーですので、ReadError
に変換するFrom
トレイトも実装しています。
SMFにかける橋
ところで、せっかくパーサーを作ったのだから、書き出しも揃えたくなりませんか?というわけでその完成形がこちら。
pub struct Writer {
messages: Vec<Message>,
// 中略
}
impl Writer {
pub fn new() -> Writer {
Writer {
messages: Vec::new(),
// 中略
}
}
pub fn push(&mut self, message: Message) {
self.messages.push(message);
}
// 中略
pub fn write(&self, path: &str) -> Result<(), io::Error> {
let path = path::Path::new(path);
let mut file = io::BufWriter::new(fs::OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path)?);
// 中略:ヘッダー書き込み
for message in self.messages.iter() {
match *message {
Message::TrackChange => {
file.write(&Message::TrackChange.binary())?;
// 中略:トラックサイズの書き込み
}
_ => {
// ランニングステータスに関する処理は省略しています
file.write(&message.binary())?;
}
}
}
Ok(file.flush()?)
}
}
Message
を事前に配列の形で登録しておき、順に書き出しています。Message
をバイナリ化する面倒な処理は全てMessage
に実装したfn binary(...)
に分離することで見通しを良くしています。なお、std::io::BufWriter
でファイルへの書き出しを行う場合、ライフタイムの終わりでエラーが出ても無視されてしまうため、最後にfn flush(...)
を呼ぶのがお作法となっています。
- κeen氏「Rustでエラーが出てないのにファイルに書き出せないときは」(個人ブログ、2017年6月)
貨物検査とハードボイルド・ワンダーランド
時代は便利になったもので、RustではCargoにより3種類のテスト11を書けます。例えば以下はtestsモジュールに記述したテスト。構造体VLQ
及びVLQBuilder
を記述しているファイルsrc/formats.rs
に一緒に記述しています。
#[cfg(test)]
mod vlq_tests {
use formats::*;
#[test]
fn binary_98327() {
let tester = VLQBuilder::new().push(134).push(0b10000000).push(23).build();
assert_eq!(tester.val(), 98327);
assert_eq!(tester.len(), 3);
assert_eq!(tester.binary(), [134, 0b10000000, 23]);
}
// 中略:境界値テスト等
}
ビット演算を多用するVLQもこれで(そこそこ)安心。また、tests
ディレクトリを作成してその中に結合テストを書くこともできます。例えば、以下のテストをtests/test.rs
に記述しています。
extern crate byteorder;
extern crate ghakuf;
use byteorder::{BigEndian, WriteBytesExt};
use ghakuf::messages::*;
use ghakuf::writer::*;
use std::fs::{OpenOptions, File};
use std::io::prelude::*;
use std::io::Read;
#[test]
fn build_integration_testing() {
let mut writer = Writer::new();
let test_messages = test_messages();
for message in test_messages {
writer.push(message);
}
assert!(writer.write("tests/test_build.mid").is_ok());
let mut data_write = Vec::new();
let mut f = File::open("tests/test_build.mid").unwrap();
f.read_to_end(&mut data_write).unwrap();
let mut data_read = Vec::new();
let mut f = File::open("tests/test.mid").unwrap();
f.read_to_end(&mut data_read).unwrap();
if data_read.len() == 0 || data_write.len() == 0 {
assert!(false);
}
assert_eq!(data_read, data_write);
}
fn test_messages() -> Vec<Message> {
let mut test_messages: Vec<Message> = Vec::new();
let tempo: u32 = 60 * 1000000 / 102; //bpm:102
test_messages.push(Message::MetaEvent {
delta_time: 0,
event: MetaEvent::SetTempo,
data: [(tempo >> 16) as u8, (tempo >> 8) as u8, tempo as u8].to_vec(),
});
test_messages.push(Message::MetaEvent {
delta_time: 0,
event: MetaEvent::EndOfTrack,
data: Vec::new(),
});
test_messages.push(Message::TrackChange);
// 中略
test_messages
}
そして、メソッド等の先頭に記述するドキュメントにもテストを書くことができます。以下のドキュメント、テスト及びコードは、src/reader.rs
に記述しています。
/// Parses SMF messages and fires(broadcasts) handlers.
///
/// # Examples
///
/// ```
/// use ghakuf::messages::*;
/// use ghakuf::reader::*;
///
/// let mut reader = Reader::new(
/// Box::new(HogeHandler {}),
/// "tests/test.mid",
/// ).unwrap();
/// let _ = reader.read();
///
/// struct HogeHandler {}
/// impl Handler for HogeHandler {
/// // 中略
/// }
/// ```
pub fn read(&mut self) -> Result<(), ReadError> {
// 中略
}
今回記述したテストはコードカバレッジを考慮していませんが、重要な部分の動作確認はなんとなく大体それなりに行っています。
「?」「!」
それでは、ライブラリが完成したので全世界に生き恥をさらしましょう。今回は、バージョン管理をGitで行った上で、コードをGitHubで公開しています。Git・GitHubの概要や使い方は、以下のサイトがわかりやすくてオススメです。
- ay3氏「GitHub 入門」(Qiita記事、2016年9月)
GitHubでの公開時は、レポジトリにREADME.mdを用意して使用例等を書いておくと、トップに表示されて格好良いかもしれません。ライセンスを記述したファイルも忘れずにご用意を。
また、CargoにはRustのコメントからドキュメントを作成する、Javadoc類似の機能が備わっています。複雑なルールがあるわけではないため、以下のサイトを参考に、簡単に作成してみました。
- Jeremiah Peschka氏「Writing Documentation in Rust」(個人ブログ、2016年5月)
このドキュメントは、後述するcrates.ioへの登録時にdocs.rsで公開することができます。
作成したライブラリは、crates.ioに登録すれば、他の人が簡単に使えるようになります。登録方法は以下を参照。
- 「Publishing on crates.io」(Cargo公式サイト)
- κeen氏「Rustのパッケージをcrates.ioに登録する」(個人ブログ、2016年1月)
Eppur ci muove!
ところで、GitHubやcrate.ioで「build passing」というバッヂを見かけたことはありませんか?あれは、継続的インテグレーションのテスト結果を表示するものです。今回は、GitHubと連携して、Linux環境とMac環境でのテストを行えるTravis CIと、Window環境でのテストを行えるAppVeyorを試しました。
Travis CIは、当該サイト上でGitHubと連携させた上で、レポジトリに.travis.yaml
を配置することでオンラインテストを行えます。.travis.yaml
は以下のサイトを参考に設定しました。
- 松島浩道氏「GitHubと連携できる継続的インテグレーションツール「Travis CI」入門」(さくらのナレッジ記事、2016年2月)
- nozaq氏「RustプロジェクトのCI設定 - テスト実行からカバレッジ計測まで」(Qiita記事、2017年3月)
AppVeyorも、当該サイト上でGitHubと連携させた上で、レポジトリにappveyor.yml
を作成することでオンラインテストを行えます。appveyor.yaml
は以下のサイトを参考に設定しました。
- κeen氏「travisとappveyorでクロスプラットフォームなCIする話」(個人ブログ、2015年12月)
設定ファイルは、上述のサイトのほか、crates.ioで上位に表示されているライブラリのレポジトリに登録されているものも参考にしています。なお。AppVeyorは、セキュリティの設定が変わったのか、VM上でRustコンパイラをダウンロードしようとすると2017年7月下旬からエラーが出るようになりましたが、以下のQ&Aを参考に設定を追加することで回避できました。
- Igor氏、Feodor Fitsner氏「Curl can't download files through SSL」(AppVeyor support center、2017年7月)
または私は如何にして心配するのを止めてRustを愛するようになったか
年年歳歳花相似、歳歳年年IT不同。プログラミング言語は百代の過客にして、行かふ技術も又旅人也。電子情報工学の進歩には目覚ましいものがありますが、当面はRustとお付き合いしていきたいと思います。最終的には、VSTホストやら歌声合成ライブラリやらを組みたいのですが、何年後になることやら12。ただ、この脳内ブームはしばらく続きそうです。
最後に、短いですが謝辞を。初心者の私が(拙いとはいえ)ライブラリを組めたのは、一重にSMF・Rustの解説記事を公開してくださっている皆さまのおかげです。特にわいやぎ氏、κeen氏におかれましては、足を向けて寝られません。願わくば、さらにこの記事がSMF界、Rust界の教師・反面教師として誰かの糧にならんことを。
-
良い記事を書くためのガイドライン?知らんなぁ……。 ↩
-
koher氏「null安全でない言語は、もはやレガシー言語だ」(Qiita記事、2016年11月)を参照。 ↩
-
Musical Instrument Digital Interface。電子楽器や電子音源の演奏情報。2017年6月にIECの規格※になったそうですが、さすがに読む気が……。※IEC 63035:2017 ↩
-
Rustは安定版のリリース(2015年5月)から日が浅く、仕様の破壊と再構築を繰り返していた頃の情報がネット上に散見されるため、公式ガイドブックが最も頼りになります。 ↩
-
やめて!石を投げないで! ↩
-
Rustの列挙型(enum)は様々な型のうち一つを選択するもので、他言語ではタグ付き共用体とも呼ばれているようです。 ↩
-
特に意味はない ↩
-
Rust公式ガイドブックには特に記述されていません。再見。 ↩