Last active
December 27, 2016 07:58
-
-
Save yuiAs/1fc2b255c7fe62921dab6b94d4e37c5c to your computer and use it in GitHub Desktop.
WASAPI共有モードの覚え書き
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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