Skip to content

Instantly share code, notes, and snippets.

@yuiAs
Last active December 27, 2016 07:58
Show Gist options
  • Select an option

  • Save yuiAs/1fc2b255c7fe62921dab6b94d4e37c5c to your computer and use it in GitHub Desktop.

Select an option

Save yuiAs/1fc2b255c7fe62921dab6b94d4e37c5c to your computer and use it in GitHub Desktop.
WASAPI共有モードの覚え書き
#include <fstream>
#include <memory>
#include <string>
#include <tuple>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <mmdeviceapi.h>
#include <Audioclient.h>
#include <wrl.h>
#include "spdlog/spdlog.h"
static std::string kLoggerName = "wasapi";
static std::string kWaveFilePath = "data/201604100030000102.wav";
HRESULT OutputHResultFormatMessage(const HRESULT hr)
{
LPSTR formated = nullptr;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM, nullptr, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), reinterpret_cast<LPSTR>(&formated), 0, nullptr);
auto localFree = [](HLOCAL pv) { LocalFree(pv); };
std::unique_ptr<std::remove_pointer<LPSTR>::type, decltype(localFree)> holder(formated, localFree);
if (holder) {
auto logger = spdlog::get(kLoggerName);
logger->error("{:#08x} {}", static_cast<uint32_t>(hr), holder.get());
}
return hr;
}
// RAII wrapper
class CoInitializeWrapper
{
public:
CoInitializeWrapper() = delete;
CoInitializeWrapper(DWORD coInit) :
m_hr(CoInitializeEx(nullptr, coInit))
{}
virtual ~CoInitializeWrapper() {
if (SUCCEEDED(m_hr)) {
CoUninitialize();
}
}
operator HRESULT() const { return m_hr; }
private:
HRESULT m_hr;
};
int main(int argc, char** argv)
{
auto logger = spdlog::stdout_logger_mt(kLoggerName);
logger->set_level(spdlog::level::debug);
CoInitializeWrapper initializer(COINIT_APARTMENTTHREADED);
if (FAILED(initializer)) {
return OutputHResultFormatMessage(initializer);
}
HRESULT hr;
// get a multimedia device enumerator
Microsoft::WRL::ComPtr<IMMDeviceEnumerator> deviceEnumerator;
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&deviceEnumerator));
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
}
// select the default device
Microsoft::WRL::ComPtr<IMMDevice> device;
hr = deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device);
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
}
Microsoft::WRL::ComPtr<IAudioClient> audioClient;
hr = device->Activate(__uuidof(**(&audioClient)), CLSCTX_INPROC_SERVER, nullptr, &audioClient);
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
}
// TODO: nBlockAlignは後で使うので保存
WAVEFORMATEXTENSIBLE format = {};
format.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE);
format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
format.Format.nChannels = 2;
format.Format.nSamplesPerSec = 48000;
format.Format.wBitsPerSample = 16;
format.Format.nBlockAlign = (format.Format.wBitsPerSample/8) * format.Format.nChannels;
format.Format.nAvgBytesPerSec = format.Format.nSamplesPerSec * format.Format.nBlockAlign;
format.Samples.wValidBitsPerSample = format.Format.wBitsPerSample;
format.dwChannelMask = SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT;
format.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
logger->info("channels={} samplingrate={} bits={} sample={} samples/sec={}", format.Format.nChannels, format.Format.nSamplesPerSec, format.Format.wBitsPerSample, format.Format.nBlockAlign, format.Format.nAvgBytesPerSec);
#if 0 // Sharedではいらない気がする
WAVEFORMATEX* closestMatch = nullptr;
hr = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, reinterpret_cast<WAVEFORMATEX*>(&format), &closestMatch);
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
}
auto coTaskMemFree = [](LPVOID pv) { CoTaskMemFree(pv); };
std::unique_ptr<WAVEFORMATEX, decltype(coTaskMemFree)> closestHolder(closestMatch, coTaskMemFree);
#endif
// REFERENCE_TIMEは 100ns単位
REFERENCE_TIME defaultDevicePeriod = 0;
REFERENCE_TIME minimumDevicePeriod = 0;
hr = audioClient->GetDevicePeriod(&defaultDevicePeriod, &minimumDevicePeriod);
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
}
logger->info("period default={}ms minimum={}ms", defaultDevicePeriod/10000.0, minimumDevicePeriod/10000.0);
// TODO: AUDCLNT_STREAMFLAGS_EVENTCALLBACKでの実装?
// hnsPeriodicityは 0の場合は defaultDevicePeriodの値が入る。基本は hnsBufferDurationと同値でいいっぽい?
// Exclusiveで AUDCLNT_STREAMFLAGS_EVENTCALLBACKを使う場合は hnsBufferDurationと同値にしなければいけないみたい
hr = audioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_NOPERSIST, defaultDevicePeriod, defaultDevicePeriod, reinterpret_cast<WAVEFORMATEX*>(&format), nullptr);
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
}
UINT32 numBufferFrames;
hr = audioClient->GetBufferSize(&numBufferFrames);
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
}
logger->info("IAudioClient::GetBufferSize {}frames", numBufferFrames);
Microsoft::WRL::ComPtr<IAudioRenderClient> renderClient;
hr = audioClient->GetService(IID_PPV_ARGS(&renderClient));
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
}
std::ifstream is(kWaveFilePath, std::ios::binary|std::ios::in);
if (!is.is_open()) {
logger->error("{} open failed", kWaveFilePath);
return -1;
}
// Read RIFF Header
{
std::vector<uint8_t> buf(12);
is.read(reinterpret_cast<char*>(&buf[0]), buf.size());
if (is.gcount() != buf.size()) {
logger->error("read failed. expect={} actual={}", buf.size(), is.gcount());
return -1;
}
if ((*reinterpret_cast<uint32_t*>(&buf[0]) != _byteswap_ulong('RIFF')) ||
(*reinterpret_cast<uint32_t*>(&buf[8]) != _byteswap_ulong('WAVE'))) {
logger->error("invalid RIFF Header.");
return -1;
}
}
// Read RIFF Chunks
std::tuple<std::streampos, std::size_t> dataPos;
{
std::vector<uint8_t> format;
std::vector<uint8_t> head(8);
while (!is.eof() && !is.fail()) {
is.read(reinterpret_cast<char*>(&head[0]), head.size());
if (is.gcount() != head.size()) {
logger->error("read failed. expect={} actual={}", head.size(), is.gcount());
return -1;
}
const uint32_t fourcc = *reinterpret_cast<uint32_t*>(&head[0]);
const uint32_t size = *reinterpret_cast<uint32_t*>(&head[4]);
logger->info("{:#08x} size={}", fourcc, size);
if (fourcc == _byteswap_ulong('fmt ')) {
std::vector<uint8_t> buf(size);
is.read(reinterpret_cast<char*>(&buf[0]), buf.size());
if (is.gcount() != buf.size()) {
logger->error("read failed. expect={} actual={}", buf.size(), is.gcount());
return -1;
}
// 確か WAVEFORMATEXは pack(1)されてた
const WAVEFORMATEX* wf = reinterpret_cast<WAVEFORMATEX*>(&buf[0]);
auto frameSize = (wf->wBitsPerSample/8) * wf->nChannels;
if (frameSize == 0) {
logger->error("unexpected frame size");
return -1;
}
logger->info("frameSize={}", frameSize);
format = std::move(std::vector<uint8_t>(sizeof(WAVEFORMATEX)));
memcpy(&format[0], wf, sizeof(WAVEFORMATEX));
} else if (format.size() && (fourcc == _byteswap_ulong('data'))) {
const auto offset = is.tellg();
logger->info("dataOffset={}", static_cast<std::streamoff>(offset));
dataPos = std::make_tuple(offset, static_cast<std::size_t>(size));
break;
} else {
is.seekg(size, std::ios_base::cur);
}
}
}
// Read data
if (!is.eof() && !is.fail()) {
static const auto wait1ms = std::chrono::microseconds(1);
// デフォルトの Bufferのサイズ
const std::size_t bufferSize = numBufferFrames * format.Format.nBlockAlign;
// デフォルトの待機時間
const auto defaultWaitTime = std::chrono::milliseconds(defaultDevicePeriod/10000);
// バッファをクリーンアップして再生開始させておく
// 事前にやっておいてもいいような
{
BYTE* p;
hr = renderClient->GetBuffer(numBufferFrames, &p);
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
}
memset(p, 0, bufferSize);
hr = renderClient->ReleaseBuffer(numBufferFrames, AUDCLNT_BUFFERFLAGS_SILENT);
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
}
hr = audioClient->Start();
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
}
}
// dataチャンクの頭に移動
is.seekg(std::get<0>(dataPos));
for (std::size_t i = 0; i < std::get<1>(dataPos);) {
if (is.eof() || is.fail()) {
break;
}
UINT32 actualFrames;
// numBufferFrames - numPaddingFramesで今書き込めるフレーム数は取得できる
// が,面倒なので未処理のフレームがまだあるなら先に処理させる
UINT32 numPaddingFrames;
hr = audioClient->GetCurrentPadding(&numPaddingFrames);
if (FAILED(hr)) {
break;
} else if (numPaddingFrames > 0) {
// std::this_thread::sleep_for(wait1ms);
// continue;
actualFrames = numBufferFrames - numPaddingFrames;
} else {
actualFrames = numBufferFrames;
}
std::size_t actualBufferSize = actualFrames * format.Format.nBlockAlign;
logger->debug("act={}", actualFrames);
// GetBufferでロックして,読み込んで,解放
BYTE* p;
hr = renderClient->GetBuffer(actualFrames, &p);
if (FAILED(hr)) {
break;
}
is.read(reinterpret_cast<char*>(p), actualBufferSize);
if (is.fail()) {
break;
}
hr = renderClient->ReleaseBuffer(actualFrames, 0);
if (FAILED(hr)) {
break;
}
i += actualBufferSize;
std::this_thread::sleep_for(defaultWaitTime);
}
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
} else if (is.fail()) {
logger->error("unexpected input stream error");
return -1;
}
hr = audioClient->Stop();
if (FAILED(hr)) {
return OutputHResultFormatMessage(hr);
}
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment