この記事は、音楽ツール・ライブラリ・技術 Advent Calendar 2018の3日目の記事です。
今回は、オーディオのタイムストレッチ/ピッチシフトを行うライブラリとして、SoundTouchとRubberBandを紹介します。
それぞれのライブラリの概要とライセンスの違い
どちらもオープンソースライブラリとして、SoundTouchはLGPL, RubberBandはGPLでソースが公開されています。
RubberBandの方はDual Licenseになっており、買い切りの有償ライセンスも用意されています。1
RubberBandの有償ライセンスはさらに「Standard Licence」と「Non-Attribution License」の2種類に分かれており、以下のように、価格と制限に違いがあります。
- Standard Licence: £420
- Non-Attribution Licenseよりも安価ですが、配布するアプリケーションで、「RubberBandライブラリを使用していることを目立つように表示する」必要があります。
- Non-Attribution License: £1120
- Standard Licenceよりも高価ですが、上記の「RubberBandライブラリを使用していることを目立つように表示する」という要件がありません。
品質の違い
できることはどちらのライブラリもだいたい同じですが、以下のような特性の違いがあります。2
- SoundTouchのほうが動作が軽量だが、音質はそこそこ
- RubberBandのほうが動作が重いが、音質はいい
SoundTouchは、タイムストレッチのアルゴリズムに、SOLA(Synchronous-OverLap-Add)という、比較的シンプルなアルゴリズムを採用しています。このアルゴリズムは、時間領域で波形をタイミングよくつなぎ合わせたり間引くことで、音の長さを変えるもので、実装がある程度シンプルにはなりますが、パーカッション系のアタックの強い音に対しては、それが繰り返されて不快な音になりやすい傾向があります。
一方RubberBandの方は、Phase Vocoderという、少し複雑なアルゴリズムを採用しています。このアルゴリズムは、周波数領域で音のピッチや時間情報を細かく制御できる利点がありますが、以下のような欠点もあります。
- 細かい間隔でFFT処理を行うために負荷が高くなりがち
- 単純な実装では処理を続けるうちに音の周波数情報が拡散して音質が徐々に悪くなることがある
RubberBandは、前者の問題についてパフォーマンスの優れたFFTライブラリを使うオプションを用意していたり、後者の問題についてもいくつかの対策を導入しているようです。
このようにそれぞれのライブラリによって特性が異なりますが、それ以外にもタイムストレッチ/ピッチシフト処理では、入力音声の特性が品質に大きく影響します。
このため、どちらのライブラリも、入力音声に合わせた設定で処理を行えるように、動作オプションを設定する機能を備えています。
この設定と入力音声の組み合わせによって、SoundTouchでも十分に音質が良くなることがありますし、逆にRubberBandでも、入力音声の特性に合わせた動作オプションを正しく設定しないと、あまり良い品質の出力は得られません。
どちらのライブラリを使用するべきか
これらを考慮すると、開発するアプリケーションの性質の違いによって以下のようにライブラリを選ぶことになります。3
- GPLで公開可能なアプリケーションで、多少負荷が高くても比較的いい音質を求めたい => RubberBand(GPL版)
- LGPLなライブラリを利用可能なアプリケーションで、負荷を軽くしつつタイムストレッチを行いたい => SoundTouch
- GPLもLGPLも避けつつ、タイムストレッチを行いたい => RubberBand (有償ライセンス版)
OSS界隈では、負荷が軽い点やライセンスがLGPLなので組み込みやすいことから、SoundTouchが使われている例が多いように見えます。(有名なオーディオ編集ソフトであるAudacityや、最近エンジンのコードが公開されたTracktionというDAWにも組み込まれています。4)
とはいえ、RubberBandの方も、筆者の方で把握していないだけで、Non-Attribution Licenseでの使用例が多くあるのかもしれません。
コード例 (SoundTouch)
以下は、SoundTouchを使用するコードの例です。
//! @param ファイル名
//! @param stretch_amount タイムストレッチ量。
//! 2を指定すると、曲の長さが2倍になる(テンポは半分になる)。
//! 0.5を指定すると、曲の長さが半分になる(テンポは2倍になる)。
//! @param cent_change_amount ピッチシフト量。100を指定すると、100cent(i.e., 1半音)ピッチを上げる。
//! @param library タイムストレッチ/ピッチシフトに使用するライブラリを指定する。
void runner(std::string filename, double stretch_amount, double cent_change_amount, Library library)
{
AudioFile<float> af_src;
af_src.load(filename);
// ...
soundtouch::SoundTouch st;
st.setSampleRate(af_src.getSampleRate());
st.setChannels(af_src.getNumChannels());
buf_dest = stretch(buf_src, stretch_amount, cent_change_amount, st);
// ...
}
template<>
Buffer<float> stretch(Buffer<float> const &src,
double stretch_amount, double cent_change_amount,
soundtouch::SoundTouch &st
)
{
assert(stretch_amount >= 0);
// SoundTouchクラスを設定
int const kProcessSize = 2048;
st.setPitchSemiTones(cent_change_amount / 100.0);
st.setTempo(1 / stretch_amount);
Buffer<float> dest(src.channels(), (int)(src.samples() * stretch_amount));
int dest_pos = 0;
std::vector<float> src_interleaved(src.channels() * kProcessSize);
std::vector<float> dest_interleaved(src.channels() * kProcessSize);
auto const length = src.samples();
for(int src_pos = 0; src_pos < length; src_pos += kProcessSize) {
auto const num_send = std::min<int>(src_pos + kProcessSize, length) - src_pos;
for(int ch = 0; ch < src.channels(); ++ch) {
for(int smp = 0; smp < num_send; ++smp) {
src_interleaved[smp * src.channels() + ch] = src.data()[ch][src_pos + smp];
}
}
// オーディオブロックをSoundTouchクラスに送信
st.putSamples(src_interleaved.data(), num_send/* * src.channels()*/);
auto const finished = (src_pos + num_send == src.samples());
if(finished) {
st.flush();
}
for( ; ; ) {
auto num_ready = st.numSamples()/* / src.channels() */;
num_ready = std::min<int>(num_ready, kProcessSize);
auto const num_receive = std::min<int>(dest_pos + num_ready, dest.samples()) - dest_pos;
if(num_receive == 0) { break; }
// SoundTouchクラスから取り出し可能な分だけオーディオデータを取り出して、dest_interleavedバッファに書き込み
st.receiveSamples(dest_interleaved.data(), num_receive/* * src.channels() */);
// dest_interleavedバッファからdestバッファにデータを転送
for(int ch = 0; ch < src.channels(); ++ch) {
for(int smp = 0; smp < num_receive; ++smp) {
dest.data()[ch][dest_pos + smp] = dest_interleaved[smp * src.channels() + ch];
}
}
dest_pos += num_receive;
}
}
return dest;
}
ここでBufferというクラスは、オーディオデータを受け渡しするために用意したバッファクラスです。一般的なオーディオアプリケーションで使われるのと同様に、チャンネルごとに用意されたサンプル列を持つ2次元配列として実装しています。
一方、SoundTouchは、データの受け渡しを、インターリーブされたサンプル列 (1次元配列のデータ上に、各チャンネルのサンプル情報が交互に並んでいるデータ構造) で行います。
そのため、SoundTouchとデータをやり取りするために、一度インターリーブされたバッファにデータを移し替える必要があります。
このようなデータ構造はWavファイルやオーディオデバイスに近い側のコードではよく見られますが、それより高レベルなAPIではチャンネルごとに配列を持っている方が扱いやすいので、インターリーブされたサンプル列を扱うことはあまりありません。
その点は、このライブラリの若干使いにくいところと言えます。
コード例 (RubberBand)
以下は、RubberBandを使用するコードの例です。
//! @param ファイル名
//! @param stretch_amount タイムストレッチ量。
//! 2を指定すると、曲の長さが2倍になる(テンポは半分になる)。
//! 0.5を指定すると、曲の長さが半分になる(テンポは2倍になる)。
//! @param cent_change_amount ピッチシフト量。100を指定すると、100cent(i.e., 1半音)ピッチを上げる。
//! @param library タイムストレッチ/ピッチシフトに使用するライブラリを指定する。
void runner(std::string filename, double stretch_amount, double cent_change_amount, Library library)
{
AudioFile<float> af_src;
af_src.load(filename);
// ...
using RB = RubberBand::RubberBandStretcher;
int const options = (RB::PercussiveOptions | RB::OptionProcessRealTime);
RubberBand::RubberBandStretcher st(af_src.getSampleRate(),
af_src.getNumChannels(),
options);
buf_dest = stretch(buf_src, stretch_amount, cent_change_amount, st);
// ...
}
template<>
Buffer<float> stretch(Buffer<float> const &src,
double stretch_amount, double cent_change_amount,
RubberBand::RubberBandStretcher &st
)
{
assert(stretch_amount >= 0);
int const kProcessSize = 2048;
// RubberBandStretcherクラスを設定
st.setMaxProcessSize(kProcessSize);
st.setPitchScale(pow(2.0, cent_change_amount / 1200.0));
st.setTimeRatio(stretch_amount);
Buffer<float> dest(src.channels(), (int)(src.samples() * stretch_amount));
int dest_pos = 0;
std::vector<float const *> src_heads(src.channels());
std::vector<float *> dest_heads(src.channels());
auto const length = src.samples();
for(int src_pos = 0; src_pos < length; src_pos += kProcessSize) {
auto const num_to_send = std::min<int>(src_pos + kProcessSize, length) - src_pos;
for(int ch = 0; ch < src.channels(); ++ch) {
src_heads[ch] = src.data()[ch] + src_pos;
}
auto const finished = (src_pos + num_to_send == src.samples());
// オーディオブロックをRubberBandStretcherクラスに送信
st.process(src_heads.data(), num_to_send, finished);
for( ; ; ) {
auto num_ready = st.available();
num_ready = std::min<int>(num_ready, kProcessSize);
if(num_ready == -1) { return dest; } //< finished
auto const num_to_receive = std::min<int>(dest_pos + num_ready, dest.samples()) - dest_pos;
if(num_ready != 0 && num_to_receive == 0) { return dest; }
if(num_to_receive == 0) { break; }
for(int ch = 0; ch < src.channels(); ++ch) {
dest_heads[ch] = dest.data()[ch] + dest_pos;
}
// RubberBandStretcherクラスから取り出し可能な分だけオーディオデータを取り出して、destバッファに書き込み
st.retrieve(dest_heads.data(), num_to_receive);
dest_pos += num_to_receive;
}
}
assert(st.available() == -1);
return dest;
}
RubberBandには、リアルタイム処理のためのRealtimeモードと、オーディオ全体を一気に処理するためのOfflineモードの2種類の動作モードがあります。どちらのモードを設定したかによって利用できるAPIやオプションが変わることがあり、そのあたりの挙動が若干分かりにくく感じます。(今回、Offlineモードでの動作は検証しきれなかったので、現状はRealtimeモードで動作するコードにしています。)
ただ、データ構造的にはチャンネルごとのインターリーブされていないサンプル列を受け渡しできるので、その点はSoundTouchよりは扱いやすいです。
サンプルプロジェクト
上記のサンプルコードの全体は、githubで公開しています。
-
https://superpowered.com/free-open-source-time-stretching-pitch-shifting ↩
-
ここではこの2つのライブラリのみを比較していますが、ほかにSound eXchange(SoX)というオープンソースライブラリにもタイムストレッチ処理の機能が含まれていたり、zplane社のELASTIQUEという有償ライブラリなどが存在しているようです。 ↩