人生を豊かにする上で欠かせないのが音楽ですが、現代の音楽制作に欠かせないのがシンセサイザーです。今回は、音楽データのデジタル表現の方法を確認した後、サウンドフォント形式に対応したシンセを作ってみましょう。

  • ピアノの演奏を行うWAVファイルを生成したところ

音楽データのデジタル表現

音楽データと聞いて思い浮かぶのは下記のような波形データではないでしょうか。ミュージシャンのレコーディング風景に必ず映っているのが、次のような波形データです。この波形データとは一体何なのでしょうか。

  • 音楽データと言えばこんな感じの波形

そもそも、音というのは空気の振動です。湖に石を落とすと、落とした場所を中心に波紋が広がりますが、それと同じように、音は空気を振動させて音源を中心に周囲へと波が伝えるのです。

それで、コンピューターで音を扱うためには、空気の振動(アナログ情報)をデジタル化(数値化)する必要があります。空気の信号をデジタル化するには、波を一定の時間間隔で区切り、その時間ごとの波の圧力(音圧)を測定します。それで、よく見る波形データは、横軸(X座標)が時間であり、縦軸(Y座標)が音圧となっています。

  • コンピューターで音を扱うには空気の振動をデジタル化する

音楽CDは1秒間に44,100回分の情報を記録しており、サンプリング周波数は44,100Hzとなります。これに対して、旧来の電話では1秒間に8,000回程度の情報を記録しており、サンプリング周波数は8,000Hzとなります。つまり、サンプリング周波数が大きいほど、それだけ滑らかで高音質となります。

Rustで簡単なサイン波のWAVファイルを作成しよう

それでは、音のデジタル表現について分かったところで、RustでWAVファイルを作成してみましょう。Rustのパッケージ(クレート)を集約したcrates.ioには、WAVファイルを手軽に扱うためのものが複数用意されています。今回は、手軽にWAVファイルを生成できるwav_ioを利用してみます。

ターミナルを起動して、以下のcargoコマンドを実行しましょう。cargoコマンドは、Rustのビルドシステムであり、パッケージマネージャーです。

# プロジェクトを作成
$ cargo init
# wav_ioをプロジェクトに追加
$ cargo add wav_io

すると、プロジェクトのひな形が作成されるので、src/main.rsというファイルが作成されます。そこで、このファイルを開いて次のように書き換えてみましょう。

以下のプログラムは、最もシンプルなサイン派と呼ばれる音(プーという感じの音)をプログラムで生成して「sine.wav」というWAVファイルに書き出すものです。

use wav_io;
use std::f32::consts::PI;

fn main() {
    // ここで作成する波形の設定 ---- (*1)
    let sample_rate = 44_100; // サンプリング周波数(CD音質)を指定
    let tone = 440.0; // 周波数(Hz) A(ラ)の音
    let volume = 0.6; // 音量(0-1)
    // WAVファイルのヘッダを生成 --- (*2)
    let mut wav_head = wav_io::new_mono_header();
    wav_head.sample_rate = sample_rate; // サンプリング周波数を設定
    // WAVデータを作成する --- (*3)
    let sample_len = (sample_rate * 2) as usize; // 2秒間のデータを作成
    let mut samples: Vec<f32> = vec![];
    for i in 0..sample_len {
        let c = i as f32 / sample_rate as f32;
        let v = (c * tone * 2.0 * PI).sin() * volume;
        samples.push(v);
    }
    // ファイルへ保存 --- (*4)
    let mut wav_out = std::fs::File::create("./sine.wav").unwrap();
    wav_io::write_to_file(&mut wav_out, &wav_head, &samples).unwrap();
}

最初にプログラムを実行してみましょう。ターミナル上で、以下のコマンドを実行します。すると「sine.wav」というWAVファイルが作成されます。

$ cargo run

Windowsならメディアプレーヤー、macOSならQuicktimeプレイヤーで再生するとプーっというサイン波が2秒再生されます。

なお、Audacityなどの波形編集ツールをインストールすると、具体的な波形を確認できます。作成したWAVファイル「sine.wav」を開いて波形を確認してみると、次の画像のように綺麗な波の波形となります。このように一定周期で繰り返すことで、安定した音階を奏でることができます。

  • 作成されたsine.wavを再生しているところ

プログラムを確認してみましょう。(*1)ではサンプリング周波数を指定します。ここでは、CD音質の44,100Hzを指定しました。また、サイン波の周波数を440.0Hzとしていますので、ラの音が書き込まれます。ここを、523.251などと変更すると、少し上のドの音になります。

(*2)ではWAVファイルのヘッダを生成します。そして、(*1)で指定したサンプリング周波数である44,100Hzを指定します。

(*3)では2秒分のサイン波を生成します。44,100Hzとはつまり1秒間に44100個のデータがあることで、2秒分なら44100*2=88200個のデータを指定することになります。なお、WAVファイルのデータには、実数(f32型)で-1.0から1.0までのデータを与えます。ここでは、f32型のsinメソッドを利用してサイン波を作成しています。

そして、(*4)でファイルへ保存します。wav_ioクレートの、write_to_file関数を使う事で、作成した波形をWAV形式でファイルに保存できます。

サウンドフォントをダウンロードしよう

ところで、プーとかポートかシンプルな音を鳴らすだけでは面白くありません。せっかくなので、ピアノやギターなどの音を鳴らしてみたいものです。今回は、伝統的なシンセサイザーの音源データであるサウンドフォント(SoundFont)を利用して、ピアノを演奏するシンセを作ってみましょう。

なお、サウンドフォントを使ってWAVファイルを作成する場合も、サイン波を計算して書き込んでいた部分、上記のプログラムで言えば(*3)の部分を、サウンドフォントを使った波形計算に変えるだけです。

インターネット上では、さまざまなサウンドフォントが配布されています。「SoundFont」あるいは「sf2/sf3」などで検索してみましょう。今回は、オープンソースのシンセデータであり、6MBという小サイズのサウンドフォント「TimGM6mb.sf2」をこちらからダウンロードして使ってみましょう。ちなみに、サウンドフォントについては、こちらに入手可能サイトの一覧がまとまっています。

RustySynthでサウンドフォントを操ろう

サウンドフォントの扱いは、それなりに大変ですが、今回は、RustySynthというクレートを利用してみましょう。ターミナルで以下のコマンドを実行して、プロジェクトを作成し、必要なクレートをインストールしましょう。

# プロジェクトを作成
$ cargo init
# クレートを追加
$ cargo add rustysynth
$ cargo add wav_io

次に「src/main.rs」を開いて、次のプログラムを記述しましょう。これは、ドミソの和音と、レファラの和音をピアノで演奏するWAVファイルを作成するプログラムです。こちらにプログラムをアップしてあります。

use std::fs::File;
use std::sync::Arc;
use rustysynth::{SynthesizerSettings, Synthesizer, SoundFont};
use wav_io;
fn main() {
    // サンプリング周波数(CD音質)を指定 --- (*1)
    const SAMPLE_RATE: u32 = 44_100;
    const SAMPLE_LEN: usize = (SAMPLE_RATE * 2) as usize; // 2秒分のデータ
    // データの書き込み先を準備 --- (*2)
    let mut samples:Vec<f32> = vec![];
    let mut left_buf = [0.0f32; SAMPLE_LEN];
    let mut right_buf = [0.0f32; SAMPLE_LEN];
    // 用意したサウンドフォントを読み込む --- (*3)
    let mut sf2 = File::open("TimGM6mb.sf2").unwrap();
    let sound_font = Arc::new(SoundFont::new(&mut sf2).unwrap());
    // シンセサイザーの作成 --- (*4)
    let settings = SynthesizerSettings::new(SAMPLE_RATE as i32);
    let mut synthesizer = Synthesizer::new(&sound_font, &settings).unwrap();
    // 音量や音色を変更 --- (*5)
    synthesizer.set_master_volume(1.0); // 音量を設定
    synthesizer.process_midi_message(0, 0xC0, 0, 0); // ピアノ(no:0)に設定
    // ドミソの和音を鳴らす --- (*6)
    synthesizer.note_on(0, 60, 120); // ド (ch, note, velocity)
    synthesizer.note_on(0, 64, 120); // ミ
    synthesizer.note_on(0, 67, 120); // ソ
    // バッファに波形を書き込む --- (*7)
    synthesizer.render(&mut left_buf, &mut right_buf);
    // WAVファイル保存用に左右チャンネルのデータを書き込み --- (*8)
    for i in 0..SAMPLE_LEN {
        samples.push(left_buf[i]);
        samples.push(right_buf[i]);
    }
    // ドミソの音を消す --- (*9)
    synthesizer.note_off_all(true);
    // レファラの音を鳴らす --- (*10)
    synthesizer.note_on(0, 62, 120);
    synthesizer.note_on(0, 65, 120);
    synthesizer.note_on(0, 69, 120);
    synthesizer.render(&mut left_buf, &mut right_buf);
    for i in 0..SAMPLE_LEN {
        samples.push(left_buf[i]);
        samples.push(right_buf[i]);
    }
    synthesizer.note_off_all(true);
    // WAVファイルを保存 --- (*11)
    let mut wav_head = wav_io::new_stereo_header();
    wav_head.sample_rate = SAMPLE_RATE;
    let mut wav_out = std::fs::File::create("./piano.wav").unwrap();
    wav_io::write_to_file(&mut wav_out, &wav_head, &samples).unwrap();
}

プログラムを書き込んだら、次に、先ほどダウンロードしたサウンドフォントのファイルをコピーしましょう。次のようなフォルダ構成となります。

.
├── Cargo.toml  ---  プロジェクトの設定ファイル
├── TimGM6mb.sf2  ---  サウンドフォント
└── src
   └── main.rs  --- メインファイル

そして、ターミナルで次のように実行します。すると、「piano.wav」というWAVファイルが出力されます。メディアプレイヤーなどで再生してみてください。ピアノで「ドミソ」と「レファラ」が演奏されます。

$ cargo run

なお、このファイルを、波形編集ツールで開くと次のようになります。

  • ピアノで和音を書き込んだところ

プログラムを確認してみましょう。(*1)ではサンプリング周波数の指定と2秒分のサンプル数を計算します。

(*2)の部分では波形データを書き込むための変数を用意します。なお、rustysynthでは左右2チャンネルのステレオデータを作成します。そのため、left_buf(左チャンネル用)とright_buf(右チャンネル用)と二つを用意します。また、Vec型のsamplesはファイル書き込みように利用します。

(*3)ではサウンドフォントを読み込みます。そして、(*4)ではサウンドフォントを使って音を鳴らすためのシンセサイザーを作成します。なお、このシンセサイザーは、サウンドフォントを読み込んで、指定された音色の波形データをバッファに書き込むだけの働きしかありません。そのため、音長などの指定はできません。

それで、(*5)では音量や音色の変更を行ったら、(*6)のように鍵盤を押す(note_on)、(*9)のように鍵盤を離す(note_off)といった操作をRustのプログラムで記述します。なお、note_onメソッドでは、音の高さを0から127の整数で指定します。

(*7)ではシンセサイザーでバッファに波形を書き込みます。そして、(*8)ではWAVファイル用に波形データを変数samplesに書き込みます。ステレオのWAVファイルでは、左、右、左、右…と交互に波形データを書き込む必要があるので、このようにfor文を使って交互に書き込みます。

(*10)ではレファラの和音を書き込みます。発音する音程が違うだけで(*6)から(*9)と同じ手順です。そして、最後に(*11)ではWAVファイルに波形を保存します。

まとめ

以上、今回はサウンドフォントを利用して、ピアノの演奏を行うWAVファイルを作成するプログラムを作ってみました。さすがに、サウンドフォントを扱うプログラムは、少し長いものになりましたが、RustySynthクレートを使うことで、比較的簡単にサウンドフォントを読み込み波形データを書き込むことができました。

とは言え、今回は音を鳴らしただけでしたので、演奏という感じはありませんでした。次回、演奏データを読み出して再生するプログラムに挑戦しましょう。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。技術書も多く執筆している。直近では、「実践力をアップする Pythonによるアルゴリズムの教科書(マイナビ出版)」「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」「すぐに使える!業務で実践できる! PythonによるAI・機械学習・深層学習アプリのつくり方 TensorFlow2対応(ソシム)」「マンガでざっくり学ぶPython(マイナビ出版)」など。