はじめに
libao はCで書かれたクロスプラットフォームなPCMデータ再生用のライブラリです。
ほぼ再生以外の機能はありませんが、その分お手軽に使えるのでちょっとした実験などには便利そうです。
この文章は公式ドキュメントの libao Overview の内容を自分なりに纏めたもの+αです。
とりあえず再生してみる
実際に音を鳴らすコードは次のようになります。
# include <cmath>
# include <cstdint>
# include <vector>
# include <ao/ao.h>
int main() {
// 定数
constexpr double PI = 3.14159265358979323846; // π
constexpr unsigned int RATE = 44100; // サンプリングレート
constexpr size_t FRAMES = RATE * 2; // 波形のサンプル数
// 波形作成 (440Hz のサイン波)
std::vector<int16_t> wave(FRAMES);
for (size_t i = 0; i < FRAMES; i++) {
wave[i] = 30000 * std::sin(440.0 * 2.0 * PI * i / RATE);
}
// libao 初期化
ao_initialize();
// 出力フォーマットの設定 (16bit/44kHz/mono)
ao_sample_format format;
format.bits = 16;
format.rate = RATE;
format.channels = 1;
format.byte_format = AO_FMT_NATIVE;
format.matrix = nullptr;
// デフォルトドライバでデバイスを開く
ao_device* device;
int id = ao_default_driver_id();
device = ao_open_live(id, &format, nullptr);
// 再生
ao_play(device, reinterpret_cast<char*>(wave.data()), FRAMES * format.bits / 8);
// あとしまつ
ao_close(device);
ao_shutdown();
}
初期化: ao_initialize()
ライブラリを初期化します。出力に使うプラグインや設定ファイルがあればここで読み込まれます。
参考までに Xubuntu18.04 で用意されていた設定ファイル /etc/libao.conf は次のようになっていました。
default_driver=pulse
quiet
デフォルトドライバ(後述)に pulse を使用、またエラーログを出力しない設定です。
デバイスを開く: ao_open_live()
ao_open_live()
または ao_open_file()
で出力するために使うデバイスを開きます。
ao_open_live()
の場合は 使用するドライバのID、サンプルフォーマット、オプションの3つ指定します
ao_open_file()
の場合は上記に加えてファイル名を指定します。
libao では プラットフォーム上で再生する機能をドライバ、ドライバを利用して実際に出力する機能を提供するものをデバイスと呼び区別しています。
またドライバにはサウンドカードなどでの再生用と、ファイルに出力用の2種類がありこちらも区別されています。
ao_open_live()
は再生用、ao_open_file()
はファイル出力用のドライバを使用する場合に使います。
ドライバID
ao_open
でドライバを指定するには、ドライバの ID を指定します。
IDの取得には ao_default_driver_id()
や ao_driver_id()
、ao_dirver_info_list()
を使用します。
ao_default_driver_id()
はその環境で使える再生用のドライバのIDを返してくれます。とりあえず音を鳴らしたい場合はこれ。
どのドライバが使われるかは設定ファイルの指定があればそちらを、無い場合は出力ドライバ毎に決められている優先順位をもとに決定されるます。
見つからない場合は何もしないダミー用のドライバになります。
特定のドライバを使いたい場合は ao_driver_id()
を使用します。
ALSAなら "alsa"、Waveファイルなら "wav" など、ドライバごとに短い名前がつけられているので ao_driver_id()
でIDに変換し使用します。
ao_driver_info_list()
はすべてのドライバ情報を取得します。
ao_info*
の配列(というかダブルポインタ)が返ってきますが、添字がそのままIDです。
サンプルフォーマットの設定
1サンプルあたりのビット数や、サンプリングレートなど出力する波形のフォーマットを指定します。
ビット数については、8/16/24/32 などを指定しますが、何が使えるかはドライバ次第です。
8bit については、いくつかソースを見たところ符号あり/なし両方みかけました。24bitや32bitもドライバ次第のようなので、基本的には 16bit 符号あり整数を使うのが良さそうです。
多チャンネルのデータについては、wave[frames][channel]
のような並びになります。
どのチャンネルをどのスピーカーに送るか指定するため、"L,R,C,LFE,BR,BL"のような文字列を matrix で指定できます。
とくに指定しない場合は null
を指定します。
オプション
ドライバ毎の動作を制御できます。
が今回は特に必要なかったので、試していません。
どのような項目があるのかは libao Drivers を参照してください。
再生: ao_play()
再生に使うデバイス、波形データ、波形データサイズ(bytes) を指定指定します。
データの転送が終わるまで処理は返ってきません。
連続して呼び出せば音は途切れずに再生できます。
あとしまつ: ao_close()
ao_shutdown()
ao_close()
デバイスを閉じ、使用されていたメモリを開放します。
ao_shutdown()
プラグインの開放やライブラリ内部で使われたメモリを開放します。
別スレッドで再生してみる
ao_play
は再生が終わるまで処理が返ってきません。
これはこれで便利なのですが、波形を生成しながら再生したい場合もあります。
下のプログラムでは、再生/波形生成用のスレッドを作成して音声を再生しつつ、メインスレッドから生成する波形を制御しています。
# include <ao/ao.h>
# include <atomic>
# include <condition_variable>
# include <functional>
# include <memory>
# include <mutex>
# include <thread>
# include <vector>
// libao 初期化
struct Ao {
Ao() { ao_initialize(); }
~Ao() { ao_shutdown(); }
};
// ao_device は unique_ptr で管理
struct Device : private std::unique_ptr<ao_device, int (*)(ao_device *)> {
using base_t = std::unique_ptr<ao_device, int (*)(ao_device *)>;
Device() : base_t(nullptr, ao_close) {}
void open(int driver_id, ao_sample_format *format,
ao_option *options = nullptr) {
base_t::reset(ao_open_live(driver_id, format, options));
}
void open(int driver_id, char const *filename, int overwrite,
ao_sample_format *format, ao_option *options = nullptr) {
base_t::reset(
ao_open_file(driver_id, filename, overwrite, format, options));
}
int close() { return ao_close(release()); }
using base_t::get;
using base_t::release;
using base_t::swap;
using base_t::operator bool;
};
class Player {
public:
using buffer_t = std::vector<uint8_t>;
using lock_t = std::unique_lock<std::mutex>;
using renderer_t = std::function<void(uint8_t *, size_t)>;
explicit Player(ao_device *device = nullptr) : device_(device) {}
~Player() { stop(); }
void device(ao_device *device) {
stop();
device_ = device;
}
// 再生開始
void start(size_t block_size, size_t block_count, renderer_t renderer) {
if (!device_)
return;
stop();
buffer_.clear();
buffer_.resize(block_size * block_count, 0);
block_size_ = block_size;
block_count_ = block_count;
quit_ = false;
remains_ = block_count;
play_pos_ = 0;
render_pos_ = 0;
renderer_ = std::move(renderer);
play_thread_ = std::thread([this] { play(); });
render_thread_ = std::thread([this] { render(); });
}
// 再生を終了しスレッド終了を待つ
void stop() {
if (!device_)
return;
{
lock_t lock(mutex_);
quit_ = true;
cv_.notify_all();
}
if (play_thread_.joinable())
play_thread_.join();
if (render_thread_.joinable())
render_thread_.join();
}
// non-copyable
Player(Player const &) = delete;
Player &operator=(Player const &) = delete;
private:
ao_device *device_;
buffer_t buffer_;
size_t block_size_;
size_t block_count_;
size_t quit_;
size_t remains_;
size_t play_pos_;
size_t render_pos_;
renderer_t renderer_;
std::mutex mutex_;
std::condition_variable cv_;
std::thread play_thread_;
std::thread render_thread_;
// 波形再生メインループ
void play() {
lock_t lock(mutex_);
for (;;) {
// バッファが追加されるか、終了が通知されるまで wait
cv_.wait(lock, [this] { return quit_ || remains_ > 0; });
if (quit_)
break;
// 再生中は一旦ロック解除
lock.unlock();
ao_play(device_,
reinterpret_cast<char *>(&buffer_[play_pos_ * block_size_]),
block_size_);
// 再ロックしバッファを消費したことを通知
lock.lock();
play_pos_ = (play_pos_ + 1) % block_count_;
remains_--;
cv_.notify_one();
}
}
// 波形生成呼び出しメインループ
void render() {
lock_t lock(mutex_);
for (;;) {
// バッファに空きができるか、終了が通知されるまで wait
cv_.wait(lock, [this] { return quit_ || (block_count_ - remains_) > 0; });
if (quit_)
break;
// 波形生成中は一旦ロック解除
lock.unlock();
if (renderer_)
renderer_(&buffer_[render_pos_ * block_size_], block_size_);
// 再ロックしバッファを追加したことを通知
lock.lock();
render_pos_ = (render_pos_ + 1) % block_count_;
remains_++;
cv_.notify_one();
}
}
};
// int16/mono 固定 矩形波出力
class Beep {
public:
Beep(float rate = 44100.f, float gain = 0.5f)
: rate_(rate), gain_(gain), pos_(0), freq_(0) {}
void freq(float freq) { freq_ = freq; }
void operator()(uint8_t *p, size_t size) {
auto buf = reinterpret_cast<int16_t *>(p);
size_t frames = size / 2;
float f = freq_;
float ny = rate_ / 2;
int16_t v = 32727 * gain_;
// 周波数または音量が0の場合は無音
if ((f <= 0) || v <= 0) {
std::fill(buf, buf + frames, 0);
return;
}
// 波形生成
for (size_t i = 0; i < frames; i++) {
pos_ = pos_ + f;
if (pos_ >= rate_)
pos_ -= rate_;
buf[i] = (pos_ <= ny) ? v : -v;
}
}
private:
float rate_;
float gain_;
float pos_;
// 周波数は複数スレッドから操作されるのでatomic
std::atomic<float> freq_;
};
int main() {
// 定数
constexpr float rate = 44100;
constexpr int block_size = 128;
constexpr int block_count = 2;
constexpr float gain = 0.5;
// libao 初期化
Ao ao;
// サウンドデバイスを開く
Device device;
ao_sample_format format;
format.bits = 16;
format.rate = rate;
format.channels = 1;
format.byte_format = AO_FMT_NATIVE;
format.matrix = nullptr;
device.open(ao_default_driver_id(), &format);
// 再生
Beep beep(rate, gain);
Player player(device.get());
player.start(block_size, block_count, std::ref(beep));
// 適当に鳴らす
for (int i = 20; i > 0; i--) {
int t = i * i / 4;
for (int j = 0; j < t; j++) {
beep.freq(10000 * j / t + 1);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
}
追記
2019-03-11
- 「別スレッドで再生してみる」のソースコードで、
Beep
がPlayer
より先に消えていたのを修正 - その他気になったところを修正