音声フレームを取得する
SkyWay Linux®︎ SDK (以下、Linux SDK ) では、v3.2.0 から音声フレームの取得が行えます。
注意 本ドキュメントはクイックスタートを実施済みの方を対象としております。
利用方法
Subscribeした AudioStream ごとに音声データとして出力することができます。
以降の説明では、表記の簡略化のため、 skyway::media::stream::remote 名前空間を省略します。
音声データは RemoteAudioStream::Listener を継承したクラスを RemoteAudioStream::RegisterListener に登録することで
RemoteAudioStream::Listener::OnTrackData から取得することができます。
OnTrackDataは10ms 周期で呼び出されます。
class AudioTrackListener : public skyway::media::stream::remote::RemoteAudioStream::Listener { public: AudioTrackListener(); // データを取得した際、こちらのメソッドがコールバックとして呼ばれます。 // 引数には受信したデータが入ります。 void OnTrackData(const skyway::media::audio::interface::AudioFrame& audio_frame) override; };
ダミースピーカーの利用
注意 音声を取得したい場合は何らかのデバイスをSetPlayoutDeviceで選択する必要があります。
PulseAudioが無効な環境においても、DummySpeakerを選択可能です。
音声の入力とは異なり、PulseAudioが有効な場合においても、デバイスで音声を再生しながら音声データを取得することができます。 一方で、デバイスで音声を再生したくない場合は、以下のようにDummySpeakerを利用できます。
auto devices = skyway::media::DeviceManager::GetPlayoutDevices(); auto device = std::find_if(devices.begin(), devices.end(), [this](const skyway::media::DeviceManager::AudioDevice& device) { return device.name == "DummySpeaker"; }); skyway::media::DeviceManager::SetPlayoutDevice(*device);
サンプルコード
以下は、クイックスタートで作成するアプリを元にしたサンプルコードです。
このサンプルでは、Subscribeした AudioStream ごとに音声データを取得し、WAVファイルとして保存します。
まず、 RemoteAudioStream::Listener を継承したクラスを作成します。
// pcm_wav_recorder.hpp #include <memory> #include <mutex> #include <string> #include "skyway/media/audio/interface/audio_frame.hpp" #include "skyway/media/stream/remote/remote_audio_stream.hpp" #include "wav_writer.hpp" class PcmWavRecorder : public skyway::media::stream::remote::RemoteAudioStream::Listener { public: PcmWavRecorder(std::string publication_id); void OnTrackData(const skyway::media::audio::interface::AudioFrame& audio_frame) override; private: void EnsureOpened_(const skyway::media::audio::interface::AudioFrame& audio_frame); private: mutable std::mutex mtx_; std::string output_filepath_; std::unique_ptr<WavWriter> writer_; bool initialized_ = false; };
// pcm_wav_recorder.cpp #include "pcm_wav_recorder.hpp" #include <chrono> #include <ctime> #include <iomanip> #include <iostream> #include <sstream> PcmWavRecorder::PcmWavRecorder(std::string publication_id) : output_filepath_(publication_id + ".wav") {} void PcmWavRecorder::OnTrackData(const skyway::media::audio::interface::AudioFrame& frame) { { std::lock_guard<std::mutex> lg(mtx_); if (!initialized_) { EnsureOpened_(frame); initialized_ = true; } if (!writer_ || !writer_->IsWritable()) { return; } writer_->Write( frame.buffer, static_cast<size_t>(frame.samples_per_channel) * static_cast<size_t>(frame.channels)); } } void PcmWavRecorder::EnsureOpened_(const skyway::media::audio::interface::AudioFrame& frame) { if (writer_) { return; } writer_ = std::make_unique<WavWriter>(output_filepath_); if (!writer_->IsWritable()) { writer_.reset(); return; } WavFormat fmt; fmt.sample_rate = frame.sample_rate; fmt.channels = frame.channels; fmt.bits_per_sample = 16; writer_->SetOutputWavFormat(fmt); }
次にWAVファイルへ書き出すクラスを作成します。
// wav_writer.hpp #include <cstddef> #include <cstdint> #include <cstdio> #include <fstream> #include <string> inline void WriteLe16(unsigned char* dst, uint16_t v) { dst[0] = static_cast<unsigned char>(v & 0xFF); dst[1] = static_cast<unsigned char>((v >> 8) & 0xFF); } inline void WriteLe32(unsigned char* dst, uint32_t v) { dst[0] = static_cast<unsigned char>(v & 0xFF); dst[1] = static_cast<unsigned char>((v >> 8) & 0xFF); dst[2] = static_cast<unsigned char>((v >> 16) & 0xFF); dst[3] = static_cast<unsigned char>((v >> 24) & 0xFF); } struct WavFormat { uint32_t sample_rate; uint16_t channels; uint16_t bits_per_sample; }; class WavWriter { public: WavWriter(const std::string& path); ~WavWriter(); bool IsWritable() const { return ofs_.is_open(); }; bool SetOutputWavFormat(const WavFormat& fmt); bool Write(const void* samples, size_t num_samples); private: bool WriteHeaderSkeleton(); void FinalizeHeader(); std::ofstream ofs_; WavFormat fmt_; bool header_written_ = false; uint32_t data_bytes_ = 0; };
// wav_writer.cpp #include "wav_writer.hpp" #include <cstring> #include <iostream> WavWriter::WavWriter(const std::string& path) { ofs_.open(path, std::ios::binary); if (!ofs_) { std::cerr << "- [Error] WavWriter: failed to open file: " << path << std::endl; return; } } WavWriter::~WavWriter() { if (this->IsWritable()) { FinalizeHeader(); } } bool WavWriter::SetOutputWavFormat(const WavFormat& fmt) { if (!this->IsWritable() || header_written_) { return false; } if (fmt.sample_rate == 0 || fmt.channels == 0 || fmt.bits_per_sample == 0) { std::cerr << "- [Error] WavWriter: invalid format\n" << std::endl; return false; } fmt_ = fmt; header_written_ = WriteHeaderSkeleton(); return header_written_; } bool WavWriter::Write(const void* samples, size_t num_samples) { if (!this->IsWritable() || !header_written_ || !samples || num_samples == 0) { return false; } const std::streamsize bytes = static_cast<std::streamsize>(num_samples * sizeof(int16_t)); ofs_.write(reinterpret_cast<const char*>(samples), bytes); if (!ofs_) { return false; } data_bytes_ += static_cast<uint32_t>(bytes); return true; } bool WavWriter::WriteHeaderSkeleton() { unsigned char hdr[44] = {0}; std::memcpy(hdr + 0, "RIFF", 4); std::memcpy(hdr + 8, "WAVE", 4); std::memcpy(hdr + 12, "fmt ", 4); WriteLe32(hdr + 16, 16); // Subchunk1Size WriteLe16(hdr + 20, 1); // AudioFormat WriteLe16(hdr + 22, fmt_.channels); // NumChannels WriteLe32(hdr + 24, fmt_.sample_rate); // SampleRate const uint32_t byte_rate = fmt_.sample_rate * fmt_.channels * (fmt_.bits_per_sample / 8); WriteLe32(hdr + 28, byte_rate); const uint16_t block_align = static_cast<uint16_t>(fmt_.channels * (fmt_.bits_per_sample / 8)); WriteLe16(hdr + 32, block_align); WriteLe16(hdr + 34, fmt_.bits_per_sample); std::memcpy(hdr + 36, "data", 4); ofs_.write(reinterpret_cast<const char*>(hdr), static_cast<std::streamsize>(sizeof(hdr))); return ofs_.good(); } void WavWriter::FinalizeHeader() { if (!ofs_ || !header_written_) { return; } unsigned char u32[4]; WriteLe32(u32, data_bytes_); ofs_.seekp(40, std::ios::beg); ofs_.write(reinterpret_cast<const char*>(u32), 4); WriteLe32(u32, 36u + data_bytes_); ofs_.seekp(4, std::ios::beg); ofs_.write(reinterpret_cast<const char*>(u32), 4); ofs_.seekp(0, std::ios::end); }
次に、今回のサンプルで作成したファイルを CMakeLists.txt に追加します。
add_executable を以下に変更してください。
add_executable(app.out main.cpp example_room.cpp pcm_wav_recorder.cpp wav_writer.cpp)
次に、クイックスタートで作成した example_room.hpp に以下のincludeディレクティブを追加してください。
#include "pcm_wav_recorder.hpp"
その後、作成する PcmWavRecorder を保持するためのメンバ変数として以下を追加してください。
std::unordered_map<std::string, std::shared_ptr<PcmWavRecorder>> recorders_;
次に、 example_room.cpp に以下のincludeディレクティブを追加してください。
#include <skyway/media/stream/remote/remote_audio_stream.hpp>
その後、Subscribeした AudioStream から RemoteAudioStream を取得し、上記で作成した PcmWavRecorder を登録します。
AudioのSubscribe処理を以下のように変更してください。
// Subscribe if (publication->ContentType() == skyway::model::ContentType::kAudio) { auto devices = skyway::media::DeviceManager::GetPlayoutDevices(); auto device = std::find_if(devices.begin(), devices.end(), [this](const skyway::media::DeviceManager::AudioDevice& device) { return device.name == "DummySpeaker"; }); skyway::media::DeviceManager::SetPlayoutDevice(*device); auto subscription = room_member_->Subscribe(publication->Id(), subscription_options); auto audio_stream = std::dynamic_pointer_cast<skyway::media::stream::remote::RemoteAudioStream>(subscription->Stream()); auto recorder = std::make_shared<PcmWavRecorder>(publication->Id()); audio_stream->RegisterListener(recorder); recorders_.emplace(publication->Id(), std::move(recorder)); }
RemoteAudioStream::RegisterListener に登録された PcmWavRecorder の OnTrackDataメソッドが
音声データが到着するたびに呼び出され、音声データをWAVファイルとして書き出します。