【Windows】【Python】OpenCVでAudio読み込みお試し

:::note info
この記事はOpenCV Advent Calendar 2023の10日目の記事です。
:::

2年前だったか、3年前だったか、OpenCVにAudioIOサポート（試験的機能？）が追加されましたね。
たしか、将来的に、画像と音声などを扱うマルチモーダルAIの分野が伸びることを予想して追加された。みたいな背景だったような。
※色々うろ覚えで書いているので間違っていたご指摘を🙇🏻

ドキュメントが全然無かったり、Windowsしか対応していなかったりしますが、ちょっと触ってみます👀
この投稿は、OpenCV 4.8.1.78 で試しています。

### マイク入力
映像入力でおなじみの VideoCapture を利用します。
アレやコレやパラメータとサンプリングレートを指定してopen()します。
open()の第1引数がデバイス番号ですね。
```python
# マイク入力準備
cap = cv2.VideoCapture()
sampling_rate = 44100
params = np.asarray([
    cv2.CAP_PROP_AUDIO_STREAM, 0, cv2.CAP_PROP_VIDEO_STREAM, -1,
    cv2.CAP_PROP_AUDIO_DATA_DEPTH, cv2.CV_32F,
    cv2.CAP_PROP_AUDIO_SAMPLES_PER_SECOND, sampling_rate
])
cap.open(0, cv2.CAP_ANY, params)
audio_base_index = int(cap.get(cv2.CAP_PROP_AUDIO_BASE_INDEX))
```

grab()で次フレームのデータをバッファに入れて、retrieve()でバッファから取得します。
```python
while cap.isOpened():
    if not cap.grab():
        break
    # オーディオフレーム取得
    frame = np.asarray([])
    ret, frame = cap.retrieve(frame, audio_base_index)
    if not ret:
        break
```

上の2つとともに、かなり雑なデバッグ描画をくっつけたのが以下。
※波形もcv2.line()で描画しているので若干重いです。ちゃんとしたプログラム作るときには使用しないほうが良いです🦔
```python
from collections import deque

import cv2
import numpy as np


def draw_audio_waveform(
        sampling_data,
        width=854,
        height=240,
        thickness=2,
        bg_color=(255, 255, 255),
        plot_color=(255, 0, 0),
):
    original_width = len(sampling_data)
    center_y = int(height / 2)

    image = np.zeros((height, width, 3))
    cv2.rectangle(
        image,
        (0, 0),
        (original_width, height),
        bg_color,
        thickness=-1,
    )

    prev_value = None
    for index, point in enumerate(sampling_data):
        value = center_y + int(point * height)
        if prev_value is not None:
            cv2.line(
                image,
                (index - 1, prev_value),
                (index, value),
                plot_color,
                thickness=thickness,
                lineType=cv2.LINE_8,
            )
        prev_value = value

    image = cv2.resize(image, (width, height))

    return image


# マイク入力準備
cap = cv2.VideoCapture()
sampling_rate = 44100
params = np.asarray([
    cv2.CAP_PROP_AUDIO_STREAM, 0, cv2.CAP_PROP_VIDEO_STREAM, -1,
    cv2.CAP_PROP_AUDIO_DATA_DEPTH, cv2.CV_32F,
    cv2.CAP_PROP_AUDIO_SAMPLES_PER_SECOND, sampling_rate
])
cap.open(0, cv2.CAP_ANY, params)
audio_base_index = int(cap.get(cv2.CAP_PROP_AUDIO_BASE_INDEX))

input_audio = deque([], maxlen=sampling_rate * 5)  # 5秒分のバッファ
while cap.isOpened():
    if not cap.grab():
        break

    # オーディオフレーム取得
    frame = np.asarray([])
    ret, frame = cap.retrieve(frame, audio_base_index)
    if not ret:
        break

    for i in range(len(frame[0])):
        input_audio.append(frame[0][i])

    # デバッグ描画
    debug_frame = list(input_audio)[-500:]
    debug_image = draw_audio_waveform(
        debug_frame,
        width=len(debug_frame),
    )
    cv2.imshow('test', debug_image)
    key = cv2.waitKey(1)
    if key == 27:  # ESC
        break

cv2.destroyAllWindows()
```
上記スクリプトを動かすと、マイク入力に問題無ければ、以下のような表示がされると思います。
![a2opc-wqose.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140207/f1a3bc6c-278d-6059-e01a-9f0dfd35bf34.gif)

### オーディオファイル入力
open()の第1引数にデバイス番号ではなく、ファイルパスを指定します👀
```python
# オーディオファイル入力準備
audio_file_path = 'sample.wav'
cap = cv2.VideoCapture()
samplingRate = 44100
params = np.asarray([
    cv2.CAP_PROP_AUDIO_STREAM, 0, cv2.CAP_PROP_VIDEO_STREAM, -1,
    cv2.CAP_PROP_AUDIO_DATA_DEPTH, cv2.CV_32F,
    cv2.CAP_PROP_AUDIO_SAMPLES_PER_SECOND, samplingRate
])
cap.open(audio_file_path, cv2.CAP_ANY, params)
```

### Webカメラ入力 & マイク入力
これ使い方自信ないのですが、、、
それぞれ VideoCapture で開いて使用する感じ。。。かなー🤔
~~何かもっとスマートな設計にしてる気がするので、冬休みに時間あればOpenCVのソースコード追って、もう少しパラメータ指定確認します👀~~
2023/12/10追記：Webカメラ入力とマイク入力は、そもそも本質的には別デバイスなので、おそらく同じVideoCaptureからは取得できない
```python
from collections import deque

import cv2
import numpy as np


def draw_audio_waveform(
        sampling_data,
        width=854,
        height=240,
        thickness=2,
        bg_color=(255, 255, 255),
        plot_color=(255, 0, 0),
):
    original_width = len(sampling_data)
    center_y = int(height / 2)

    image = np.zeros((height, width, 3))
    cv2.rectangle(
        image,
        (0, 0),
        (original_width, height),
        bg_color,
        thickness=-1,
    )

    prev_value = None
    for index, point in enumerate(sampling_data):
        value = center_y + int(point * height)
        if prev_value is not None:
            cv2.line(
                image,
                (index - 1, prev_value),
                (index, value),
                plot_color,
                thickness=thickness,
                lineType=cv2.LINE_8,
            )
        prev_value = value

    image = cv2.resize(image, (width, height))

    return image


# マイク入力準備
audio_cap = cv2.VideoCapture()
sampling_rate = 16000
params = np.asarray([
    cv2.CAP_PROP_AUDIO_STREAM, 0, cv2.CAP_PROP_VIDEO_STREAM, -1,
    cv2.CAP_PROP_AUDIO_DATA_DEPTH, cv2.CV_32F,
    cv2.CAP_PROP_AUDIO_SAMPLES_PER_SECOND, sampling_rate
])
audio_cap.open(0, cv2.CAP_ANY, params)
audio_base_index = int(audio_cap.get(cv2.CAP_PROP_AUDIO_BASE_INDEX))

# Webカメラ準備
webcam_cap = cv2.VideoCapture(0)

input_audio = deque([], maxlen=sampling_rate * 5)  # 5秒分のバッファ
while audio_cap.isOpened():
    if not audio_cap.grab():
        break

    # オーディオフレーム取得
    frame = np.asarray([])
    ret, frame = audio_cap.retrieve(frame, audio_base_index)
    if not ret:
        break

    for i in range(len(frame[0])):
        input_audio.append(frame[0][i])

    # Webカメラ画像取得
    ret, webcam_image = webcam_cap.read()
    if not ret:
        break

    # デバッグ描画
    debug_frame = list(input_audio)[-500:]
    debug_image = draw_audio_waveform(
        debug_frame,
        width=len(debug_frame),
    )
    cv2.imshow('audio', debug_image)
    cv2.imshow('wecam', webcam_image)
    key = cv2.waitKey(1)
    if key == 27:  # ESC
        break

cv2.destroyAllWindows()

```

### 2023/12/10追記：動画ファイル入力(画像 & 音声)
Pythonサンプルばかり探して目に入っていませんでしたが、熊太郎さんがC++のサンプルのありかを教えてくれました🦔

https://x.com/hon_no_mushi/status/1733649669899882498?s=20

Pythonでも同様の使い方で、動画ファイルから映像フレームと音声チャンネル、それぞれが取得できました👀
```python
from collections import deque

import cv2
import numpy as np


def draw_audio_waveform(
        sampling_data,
        width=854,
        height=240,
        thickness=2,
        bg_color=(255, 255, 255),
        plot_color=(255, 0, 0),
):
    original_width = len(sampling_data)
    center_y = int(height / 2)

    image = np.zeros((height, width, 3))
    cv2.rectangle(
        image,
        (0, 0),
        (original_width, height),
        bg_color,
        thickness=-1,
    )

    prev_value = None
    for index, point in enumerate(sampling_data):
        value = center_y + int(point * height)
        if prev_value is not None:
            cv2.line(
                image,
                (index - 1, prev_value),
                (index, value),
                plot_color,
                thickness=thickness,
                lineType=cv2.LINE_8,
            )
        prev_value = value

    image = cv2.resize(image, (width, height))

    return image


# 動画ファイル入力準備
audio_file_path = 'sample.mp4'
cap = cv2.VideoCapture()
sampling_rate = 44100
params = np.asarray([
    cv2.CAP_PROP_AUDIO_STREAM, 0, cv2.CAP_PROP_VIDEO_STREAM, 0,
    cv2.CAP_PROP_AUDIO_DATA_DEPTH, cv2.CV_32F,
    cv2.CAP_PROP_AUDIO_SAMPLES_PER_SECOND, sampling_rate
])
cap.open(audio_file_path, cv2.CAP_ANY, params)

audio_base_index = int(cap.get(cv2.CAP_PROP_AUDIO_BASE_INDEX))
audio_total_channels = int(cap.get(cv2.CAP_PROP_AUDIO_TOTAL_CHANNELS))

input_audio_list = []
for index in range(audio_total_channels):
    input_audio_list.append(deque([], maxlen=sampling_rate * 5))

while cap.isOpened():
    if not cap.grab():
        break

    # 映像フレーム取得
    ret, image = cap.retrieve()
    if not ret:
        break

    # 音声フレーム取得
    for index in range(audio_total_channels):
        audio_frame = np.asarray([])
        ret, audio_frame = cap.retrieve(audio_frame, audio_base_index + index)
        for i in range(len(audio_frame[0])):
            input_audio_list[index].append(audio_frame[0][i])

    # デバッグ描画
    for index in range(audio_total_channels):
        debug_frame = list(input_audio_list[index])[-500:]
        debug_image = draw_audio_waveform(
            debug_frame,
            width=len(debug_frame),
        )
        cv2.imshow('channel:' + str(index + 1), debug_image)
        del debug_image
    cv2.imshow('image', image)
    key = cv2.waitKey(1)
    if key == 27:  # ESC
        break

cv2.destroyAllWindows()

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140207/4a39f509-faac-a205-d97a-3ebfbc12769f.png)


以上。