ミーティング中を検知して周囲の人間に知らせる(1)

# ミーティング中を検知して周囲の人間に知らせる(1)

私は同居人と２人で暮らしているのですが、在宅ワークをしているとミーティングの際に不意に同居人から話しかけられたりすることがあります。
これを防ぐため、ミーティングする前には一言声をかけるのですが、こんどはミーティングが終わったことを知らせるのを忘れてずっと音を抑えたまま不便な思いをさせてしまうことが何度かありました。
この問題をシステムで解決しようと思い立ちました。

内容は長くなりそうなので２部か３部構成にします。

## アプローチ

### ミーティングかどうかを検知する方法

在宅では基本的にMeetやHaddleでミーティングを行うため、ミュート、アンミュートに限らずPCのマイクの使用を検知すれば現在がミーティング中かどうかは判断できそうです。
さらに言えば、プロセスの名前等からhaddleやMeetに入っているかどうかも検知できそうです。

### 周囲の人間に知らせる方法

これはラズパイ等のマイコンとパトランプを用意し、パトランプをピカピカさせることで周知できそうです。
また、マイコンでLAN内のサーバーをホストすることで遠隔でミーティング中かどうかを知らせることもできます。

## 構成

- ラズパイ (Go)
    - ランプを光らせる用。いい感じのパトランプがなかったので、今回はLEDを光らせるだけにする。
    - GoでLチカきるので嬉しい
- WindowsPC (C++)
    - マイクの使用を検知する用
    - 今回は個人的に慣れているWindowsPCで作成する (将来的にはMacPCでも実装したい)

## マイクの使用を検知するプログラム

思ったより長くなってしまい、さすがC++だなという感じ。
Windowsプログラミングに慣れていない方はあまり見覚えのないものが多くあると思うが、これらはWindowsプログラミングで幅広く使用されるCOMと呼ばれるオブジェクトである。
詳細な解説はここでは省くが、コメントに何をやっているのかは軽く記載があるので参考にしてほしい。

``` c++
#include <iostream>
#include <array>
#include <mmdeviceapi.h>
#include <endpointvolume.h>
#include <audiopolicy.h>
#include <wrl/client.h>
#include <functiondiscoverykeys_devpkey.h>
#include <psapi.h>

#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "Mmdevapi.lib")

// マイクが入力されているかどうかを確認する閾値
// 今回は喋っているかどうかは確認しないので、非常に低い値を設定している（ノイズも拾う）
const float PEEK_VALUE_THRESHOLD = 0.00001f;

std::string lpwstr_to_str(LPWSTR str)
{
    int strLength = WideCharToMultiByte(CP_ACP, 0, str, -1, nullptr, 0, nullptr, nullptr);
    std::string result(strLength, 0);
    WideCharToMultiByte(CP_ACP, 0, str, -1, &result[0],
        strLength, nullptr, nullptr);
    return result;
}

int main() {
    // COMの初期化
    HRESULT hr = CoInitialize(nullptr);
    if (FAILED(hr)) {
        std::cerr << std::system_category().message(hr) << std::endl;
        return -1;
    }

    // デバイスの列挙
    Microsoft::WRL::ComPtr<IMMDeviceEnumerator> deviceEnumerator = nullptr;
    hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (LPVOID*)&deviceEnumerator);

    if (FAILED(hr)) {
        std::cerr << std::system_category().message(hr) << std::endl;
        return -1;
    }

    // デフォルトのデバイスを取得
    Microsoft::WRL::ComPtr<IMMDevice> defaultDevice = nullptr;
    hr = deviceEnumerator->GetDefaultAudioEndpoint(eCapture, eConsole, &defaultDevice);

    if (FAILED(hr)) {
        std::cerr << std::system_category().message(hr) << std::endl;
        return -1;
    }

    // デバイス名を取得
    Microsoft::WRL::ComPtr<IPropertyStore> propertyStore = nullptr;
    hr = defaultDevice->OpenPropertyStore(STGM_READ, &propertyStore);
    if (FAILED(hr)) {
        std::cerr << std::system_category().message(hr) << std::endl;
        return -1;
    }

    PROPVARIANT deviceName;
    PropVariantInit(&deviceName);
    hr = propertyStore->GetValue(PKEY_Device_FriendlyName, &deviceName);
    if (FAILED(hr)) {
        std::cerr << std::system_category().message(hr) << std::endl;
        return -1;
    }

    // セッションマネージャーを取得
    Microsoft::WRL::ComPtr<IAudioSessionManager2> sessionManager = nullptr;
    hr = defaultDevice->Activate(__uuidof(IAudioSessionManager2), CLSCTX_ALL, NULL, (void**)&sessionManager);

    if (FAILED(hr)) {
        std::cerr << std::system_category().message(hr) << std::endl;
        return -1;
    }

    while (true)
    {
        Sleep(100);

        // セッションの列挙
        Microsoft::WRL::ComPtr<IAudioSessionEnumerator> sessionEnumerator = nullptr;
        hr = sessionManager->GetSessionEnumerator(&sessionEnumerator);

        if (FAILED(hr)) {
            std::cerr << std::system_category().message(hr) << std::endl;
            return -1;
        }

        int sessionCount = 0;
        hr = sessionEnumerator->GetCount(&sessionCount);

        if (FAILED(hr)) {
            std::cerr << std::system_category().message(hr) << std::endl;
            return -1;
        }

        // 取得されたセッションに対してPeekを取得し、音声が入力されているかどうかを確認する
        for (int i = 0; i < sessionCount; ++i) {
            // セッションコントロールを取得
            Microsoft::WRL::ComPtr<IAudioSessionControl> sessionControl = nullptr;
            Microsoft::WRL::ComPtr<IAudioSessionControl2> sessionControl2 = nullptr;
            hr = sessionEnumerator->GetSession(i, &sessionControl);

            if (FAILED(hr)) {
                std::cerr << std::system_category().message(hr) << std::endl;
                return -1;
            }

            hr = sessionControl->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&sessionControl2);

            if (FAILED(hr)) {
                std::cerr << std::system_category().message(hr) << std::endl;
                return -1;
            }

            // Peekを取得
            Microsoft::WRL::ComPtr<ISimpleAudioVolume> simpleAudioVolume = nullptr;
            hr = sessionControl2->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&simpleAudioVolume);

            if (FAILED(hr)) {
                std::cerr << std::system_category().message(hr) << std::endl;
                return -1;
            }

            Microsoft::WRL::ComPtr<IAudioMeterInformation> audioMeter = nullptr;
            hr = simpleAudioVolume->QueryInterface(__uuidof(IAudioMeterInformation), (void**)&audioMeter);

            if (FAILED(hr)) {
                std::cerr << std::system_category().message(hr) << std::endl;
                return -1;
            }

            float peakValue = 0.0f;
            audioMeter->GetPeakValue(&peakValue);

            // 音声が入力されていない場合はスキップ
            if (peakValue <= PEEK_VALUE_THRESHOLD) {
                continue;
            }

            // セッションのプロセス名を取得
            DWORD processId;
            hr = sessionControl2->GetProcessId(&processId);

            if (FAILED(hr)) {
                std::cerr << std::system_category().message(hr) << std::endl;
                return -1;
            }

            HANDLE processHandle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
            if (processHandle == NULL) {
                std::cerr << std::system_category().message(GetLastError()) << std::endl;
                return -1;
            }

            std::array<wchar_t, 1024> processName;
            DWORD processNameLength = GetModuleFileNameExW(processHandle, NULL, processName.data(), processName.size());
            if (processNameLength == 0) {
                std::cerr << std::system_category().message(GetLastError()) << std::endl;
                return -1;
            }

            std::cout << "マイクが使用されています: デバイス名=" << lpwstr_to_str(deviceName.pwszVal) << " プロセス名=" << lpwstr_to_str(processName.data()) << " Peek値: " << peakValue << std::endl;
        }
    }

    CoUninitialize();

    return 0;
}
```

こんなかんじで使用しているデバイスとプロセス名が表示される。

![image-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1213727/adf8f20e-50c7-d473-b0e5-08c30c47943d.png)

## おわり

今回はマイクの使用を検知するところまで。
次回はラズパイで周囲に知らせるところをやる。