この記事はOpenCV Advent Calendar 2025の13日目の記事です。
これはOpenCVのアドベントカレンダーで良い内容なのか?
まあ、OpenCV使っているから良いか、、、( ´ー`)y-~~
はじめに
オーディオIOサポートは、長年ユーザーから要望があった待望?の機能でしたが、
ドキュメントは少なく、そもそも本当に使っている人はいるのか?と思わなくもない機能です。
(いや本当、要望されていたわりにコレ使っている人少なくない?)
ちなみに、オーディオも取りたくなる場面は、以下のようにちょいちょいあるため、機能が実装されたことは大変理解できます。
- 監視カメラなど映像を解析する際に、同時にどのような音が録音されているか確認したい
- 映像+音声を利用するマルチモーダルなAIを扱いたい
- OpenCVで動画を出力する際に、音声の再合成が必要で面倒くさい
※音声だけ FFmpeg で別に取得しタイムスタンプ同期しつつ再合成する など
※この要望もあったはずだけど、VideoWriter側の対応は進んでいない - etc
たぶん議論の初出が2012年前後(もっと早いかも)で、
要望が増えたのは2015年?前後なので、
実装されたころ(OpenCV4.5.5だっけ?2021年くらい?)には、みんな他のやり方になれていて、わざわざ改めて使われない説もありますが。。。
(ffmpegとかGStreamer使うとか、C++だとPortAudioやRtAudio、PythonだとLibrosaやsounddeviceなど使うとか)
最近、お仕事で久々に音声系を扱う機会があったので、このAPIの動きどーなってるんだっけ?
と思って、確認したメモを発掘した投稿です。
API使い方おさらい
動画に対してオーディオを取得する使い方の簡単なおさらいです↓
(省略)
# 映像側VideoCapture
cap_video = cv2.VideoCapture(VIDEO_PATH, cv2.CAP_ANY)
# 音声側VideoCapture
cap_audio = cv2.VideoCapture()
cap_audio.open(VIDEO_PATH, cv2.CAP_ANY, 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_base_index = int(cap_audio.get(cv2.CAP_PROP_AUDIO_BASE_INDEX))
num_channels = int(cap_audio.get(cv2.CAP_PROP_AUDIO_TOTAL_CHANNELS))
while True:
# 1フレーム分の画像取得
ret, image = cap_video.read()
if not ret:
break
# 音声取得
if cap_audio.grab():
channels = []
for ch in range(num_channels):
_, data = cap_audio.retrieve(np.array([]), audio_base_index + ch)
(省略)
VideoCapture()使う感じですね👀
# 音声側VideoCapture
cap_audio = cv2.VideoCapture()
cap_audio.open(VIDEO_PATH, cv2.CAP_ANY, 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,
]))
オーディオをオープンする際にcv2.CAP_ANYを指定するとフォールバックされて、OSによって MSMF(Microsoft Media Foundation)だったり、GStreamerなどが設定されるようです。
(冷静になって今見ると、バックエンドがOSによってガッツリ違うの不安要素しかないですね、、、)
実用する上で気になってくるのは、以下のretrieve()するときに取得できるサンプル数がいくつなのか。です。
_, data = cap_audio.retrieve(np.array([]), audio_base_index + ch)
画像が1フレーム分取得できるので、音声も1フレーム分に相当するサンプル数(30FPSで44100Hzなら1470サンプル)なのか、それともバックエンド側の内部バッファをそのまま返してくるのか🤔
取得サンプル数を確認してみる(Windows)
以下のようなプリント文を入れて確認してみます👀
import cv2
import numpy as np
# 設定
VIDEO_PATH = "sample.mp4"
SAMPLING_RATE = 44100
# 映像キャプチャ
cap_video = cv2.VideoCapture(VIDEO_PATH, cv2.CAP_ANY)
fps = cap_video.get(cv2.CAP_PROP_FPS) or 30
total_frames = int(cap_video.get(cv2.CAP_PROP_FRAME_COUNT))
samples_per_frame = int(SAMPLING_RATE / fps)
total_audio_samples = total_frames * samples_per_frame
print(f"FPS: {fps}, Samples per Frame: {samples_per_frame}")
print(f"Video backend: {cap_video.getBackendName()}")
# 音声キャプチャ
cap_audio = cv2.VideoCapture()
cap_audio.open(VIDEO_PATH, cv2.CAP_ANY, 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_base_index = int(cap_audio.get(cv2.CAP_PROP_AUDIO_BASE_INDEX))
num_channels = int(cap_audio.get(cv2.CAP_PROP_AUDIO_TOTAL_CHANNELS))
print(f"Audio backend: {cap_audio.getBackendName()}")
print(f"Audio channels: {num_channels}")
print(f"Audio sampling rate: {int(cap_audio.get(cv2.CAP_PROP_AUDIO_SAMPLES_PER_SECOND))} Hz")
print(f"Audio base index: {audio_base_index}")
frame_num = 0
while True:
ret, image = cap_video.read()
if not ret:
break
frame_num += 1
# 音声取得(1回だけ)
if cap_audio.grab():
channels = []
for ch in range(num_channels):
_, data = cap_audio.retrieve(np.array([]), audio_base_index + ch)
print(f"Frame: {frame_num}, Ch: {ch}, Data Shape: {data.shape}")
# デバッグ表示
cv2.imshow("Video", image)
if cv2.waitKey(1) == 27:
break
cap_video.release()
cap_audio.release()
cv2.destroyAllWindows()
出てきたログは以下の通り。
FPS: 30.0, Samples per Frame: 1470
Video backend: FFMPEG
[ERROR:0@0.024] global cap_ffmpeg_impl.hpp:1124 open VIDEOIO/FFMPEG: unsupported parameters in .open(), see logger INFO channel for details. Bailout
Audio backend: MSMF
Audio channels: 2
Audio sampling rate: 44100 Hz
Audio base index: 1
Frame: 1, Ch: 0, Data Shape: (1, 440)
Frame: 1, Ch: 1, Data Shape: (1, 440)
Frame: 2, Ch: 0, Data Shape: (1, 470)
Frame: 2, Ch: 1, Data Shape: (1, 470)
Frame: 3, Ch: 0, Data Shape: (1, 470)
Frame: 3, Ch: 1, Data Shape: (1, 470)
Frame: 4, Ch: 0, Data Shape: (1, 471)
Frame: 4, Ch: 1, Data Shape: (1, 471)
Frame: 5, Ch: 0, Data Shape: (1, 470)
Frame: 5, Ch: 1, Data Shape: (1, 470)
(省略)
オーディオバックエンドはMSMFになって、1回で取得できているサンプル数は440~471と若干揺れていますね👀
これはバックエンドの内部バッファのサイズぽいというか、10ms単位のチャンクですねたぶん🤔
取得サンプル数を確認してみる(Ubuntu)
同じスクリプトをUbuntuで試してみます。
pipで入るOpenCVではGStreamerを含まないビルドのため、GStreamer込みのビルドを用意して確認しています。
FPS: 30.0, Samples per Frame: 1470
Video backend: FFMPEG [ERROR:0@0.067]
global ./modules/videoio/src/cap_ffmpeg_impl.hpp (1086) open VIDEOIO/FFMPEG: unsupported parameters in .open(), see logger INFO channel for details.
Bailout Audio backend: GSTREAMER
Audio channels: 2
Audio sampling rate: 44100 Hz
Audio base index: 1
Frame: 1, Ch: 0, Data Shape: (1, 440)
Frame: 1, Ch: 1, Data Shape: (1, 440)
Frame: 2, Ch: 0, Data Shape: (1, 470)
Frame: 2, Ch: 1, Data Shape: (1, 470)
Frame: 3, Ch: 0, Data Shape: (1, 470)
Frame: 3, Ch: 1, Data Shape: (1, 470)
Frame: 4, Ch: 0, Data Shape: (1, 471)
Frame: 4, Ch: 1, Data Shape: (1, 471)
Frame: 5, Ch: 0, Data Shape: (1, 470)
Frame: 5, Ch: 1, Data Shape: (1, 470)
(省略)
こちらも同様👀
映像と同期
1フレーム分よりサンプル数が少ないので、もし映像と同期したいのであれば、内部的にバッファリングする必要がありますね。
Soraで作成した以下のテスト動画を使って確認してみます。
結果は分かり切っていますが、いったんバッファリングせずに音声再生しつつimshow()してみます👀
スクリプトは以下の通り。
import cv2
import numpy as np
import sounddevice as sd
# 設定
VIDEO_PATH = "sample.mp4"
SAMPLING_RATE = 44100
# 映像キャプチャ
cap_video = cv2.VideoCapture(VIDEO_PATH, cv2.CAP_ANY)
fps = cap_video.get(cv2.CAP_PROP_FPS) or 30
total_frames = int(cap_video.get(cv2.CAP_PROP_FRAME_COUNT))
samples_per_frame = int(SAMPLING_RATE / fps)
total_audio_samples = total_frames * samples_per_frame
print(f"FPS: {fps}, Samples per Frame: {samples_per_frame}")
print(f"Video backend: {cap_video.getBackendName()}")
# 音声キャプチャ
cap_audio = cv2.VideoCapture()
cap_audio.open(VIDEO_PATH, cv2.CAP_ANY, 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,
]))
print(f"Audio backend: {cap_audio.getBackendName()}")
audio_base_index = int(cap_audio.get(cv2.CAP_PROP_AUDIO_BASE_INDEX))
num_channels = int(cap_audio.get(cv2.CAP_PROP_AUDIO_TOTAL_CHANNELS))
# 音声再生ストリーム
stream = sd.OutputStream(
samplerate=SAMPLING_RATE, channels=num_channels, dtype="float32"
)
stream.start()
frame_num = 0
total_samples = 0
while True:
ret, image = cap_video.read()
if not ret:
break
frame_num += 1
if cap_audio.grab():
channels = []
# 音声取得(1回だけ)
for ch in range(num_channels):
_, data = cap_audio.retrieve(np.array([]), audio_base_index + ch)
channels.append(data)
# 音声再生
if channels[0] is not None and len(channels[0]) > 0:
audio_data = np.column_stack([ch.flatten() for ch in channels]).astype(np.float32)
total_samples += len(audio_data)
stream.write(audio_data) # 同期処理で再生
# デバッグ表示
video_pct = (frame_num / total_frames * 100) if total_frames > 0 else 0
audio_pct = (
(total_samples / total_audio_samples * 100) if total_audio_samples > 0 else 0
)
print(
f"Video: {frame_num}/{total_frames} ({video_pct:.1f}%) Audio: {total_samples}/{total_audio_samples} ({audio_pct:.1f}%)"
)
cv2.imshow("Video", image)
if cv2.waitKey(1) == 27:
break
stream.close()
cap_video.release()
cap_audio.release()
cv2.destroyAllWindows()
結果は以下です。音声サンプル数が1フレーム分より少ないので、映像の方が速く終わってしまいますね。
FPS: 30.0, Samples per Frame: 1470
Video backend: FFMPEG
[ERROR:0@0.017] global cap_ffmpeg_impl.hpp:1124 open VIDEOIO/FFMPEG: unsupported parameters in .open(), see logger INFO channel for details. Bailout
Audio backend: MSMF
Video: 1/450 (0.2%) Audio: 440/661500 (0.1%)
Video: 2/450 (0.4%) Audio: 910/661500 (0.1%)
Video: 3/450 (0.7%) Audio: 1380/661500 (0.2%)
Video: 4/450 (0.9%) Audio: 1851/661500 (0.3%)
Video: 5/450 (1.1%) Audio: 2321/661500 (0.4%)
Video: 6/450 (1.3%) Audio: 2792/661500 (0.4%)
Video: 7/450 (1.6%) Audio: 3262/661500 (0.5%)
Video: 8/450 (1.8%) Audio: 3732/661500 (0.6%)
Video: 9/450 (2.0%) Audio: 4203/661500 (0.6%)
Video: 10/450 (2.2%) Audio: 4673/661500 (0.7%)
Video: 11/450 (2.4%) Audio: 5144/661500 (0.8%)
Video: 12/450 (2.7%) Audio: 5614/661500 (0.8%)
Video: 13/450 (2.9%) Audio: 6084/661500 (0.9%)
Video: 14/450 (3.1%) Audio: 6555/661500 (1.0%)
(省略)
映像1フレーム分の音声をバッファリングしてから再生するように変更すると以下です。
import cv2
import numpy as np
import sounddevice as sd
# 設定
VIDEO_PATH = "sample.mp4"
SAMPLING_RATE = 44100
# 映像キャプチャ
cap_video = cv2.VideoCapture(VIDEO_PATH, cv2.CAP_ANY)
fps = cap_video.get(cv2.CAP_PROP_FPS) or 30
total_frames = int(cap_video.get(cv2.CAP_PROP_FRAME_COUNT))
samples_per_frame = int(SAMPLING_RATE / fps)
total_audio_samples = total_frames * samples_per_frame
print(f"FPS: {fps}, Samples per Frame: {samples_per_frame}")
print(f"Video backend: {cap_video.getBackendName()}")
# 音声キャプチャ
cap_audio = cv2.VideoCapture()
cap_audio.open(VIDEO_PATH, cv2.CAP_ANY, 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,
]))
print(f"Audio backend: {cap_audio.getBackendName()}")
audio_base_index = int(cap_audio.get(cv2.CAP_PROP_AUDIO_BASE_INDEX))
num_channels = int(cap_audio.get(cv2.CAP_PROP_AUDIO_TOTAL_CHANNELS))
# 音声再生ストリーム
stream = sd.OutputStream(
samplerate=SAMPLING_RATE, channels=num_channels, dtype="float32"
)
stream.start()
frame_num = 0
total_samples = 0
audio_buffer = [[] for _ in range(num_channels)]
while True:
ret, image = cap_video.read()
if not ret:
break
frame_num += 1
# 音声取得(1フレーム分集まるまで)
while len(audio_buffer[0]) < samples_per_frame:
if not cap_audio.grab():
break
for ch in range(num_channels):
_, data = cap_audio.retrieve(np.array([]), audio_base_index + ch)
if data is not None and len(data) > 0:
audio_buffer[ch].extend(data.flatten())
# 音声再生
if len(audio_buffer[0]) >= samples_per_frame:
audio_data = np.column_stack(
[
np.array(audio_buffer[ch][:samples_per_frame], dtype=np.float32)
for ch in range(num_channels)
]
)
for ch in range(num_channels):
audio_buffer[ch] = audio_buffer[ch][samples_per_frame:]
total_samples += samples_per_frame
stream.write(audio_data) # 同期処理で再生
# デバッグ表示
video_pct = (frame_num / total_frames * 100) if total_frames > 0 else 0
audio_pct = (
(total_samples / total_audio_samples * 100) if total_audio_samples > 0 else 0
)
print(
f"Video: {frame_num}/{total_frames} ({video_pct:.1f}%) Audio: {total_samples}/{total_audio_samples} ({audio_pct:.1f}%)"
)
cv2.imshow("Video", image)
if cv2.waitKey(1) == 27:
break
stream.close()
cap_video.release()
cap_audio.release()
cv2.destroyAllWindows()
結果は以下です。映像と音声がほぼ正しいタイミングで再生できていますね👀
FPS: 30.0, Samples per Frame: 1470
Video backend: FFMPEG
[ERROR:0@0.051] global cap_ffmpeg_impl.hpp:1124 open VIDEOIO/FFMPEG: unsupported parameters in .open(), see logger INFO channel for details. Bailout
Audio backend: MSMF
Video: 1/450 (0.2%) Audio: 1470/661500 (0.2%)
Video: 2/450 (0.4%) Audio: 2940/661500 (0.4%)
Video: 3/450 (0.7%) Audio: 4410/661500 (0.7%)
Video: 4/450 (0.9%) Audio: 5880/661500 (0.9%)
Video: 5/450 (1.1%) Audio: 7350/661500 (1.1%)
Video: 6/450 (1.3%) Audio: 8820/661500 (1.3%)
Video: 7/450 (1.6%) Audio: 10290/661500 (1.6%)
Video: 8/450 (1.8%) Audio: 11760/661500 (1.8%)
Video: 9/450 (2.0%) Audio: 13230/661500 (2.0%)
Video: 10/450 (2.2%) Audio: 14700/661500 (2.2%)
Video: 11/450 (2.4%) Audio: 16170/661500 (2.4%)
Video: 12/450 (2.7%) Audio: 17640/661500 (2.7%)
Video: 13/450 (2.9%) Audio: 19110/661500 (2.9%)
Video: 14/450 (3.1%) Audio: 20580/661500 (3.1%)
そーいえば、VideoCapture()一つで取得できるんだっけ?
上記のサンプルでは、映像用と音声用のVideoCapture()をそれぞれ用意していました。
APIのパラメータ上は以下のように1つのVideoCapture()でオープンできるようですが、、、
import cv2
import numpy as np
# 設定
VIDEO_PATH = "sample.mp4"
SAMPLING_RATE = 44100
# 映像+音声キャプチャ(1つのVideoCaptureで両方取得)
cap = cv2.VideoCapture()
cap.open(VIDEO_PATH, cv2.CAP_ANY, 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,
]))
fps = cap.get(cv2.CAP_PROP_FPS) or 30
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
samples_per_frame = int(SAMPLING_RATE / fps)
audio_base_index = int(cap.get(cv2.CAP_PROP_AUDIO_BASE_INDEX))
num_channels = int(cap.get(cv2.CAP_PROP_AUDIO_TOTAL_CHANNELS))
print(f"Backend: {cap.getBackendName()}")
print(f"FPS: {fps}, Samples per Frame: {samples_per_frame}")
print(f"Audio channels: {num_channels}")
print(f"Audio sampling rate: {int(cap.get(cv2.CAP_PROP_AUDIO_SAMPLES_PER_SECOND))} Hz")
print(f"Audio base index: {audio_base_index}")
frame_num = 0
while True:
if not cap.grab():
break
frame_num += 1
# 映像取得
ret, image = cap.retrieve()
if not ret:
break
# 音声取得
for ch in range(num_channels):
ret_audio, data = cap.retrieve(np.array([]), audio_base_index + ch)
if ret_audio and data is not None and data.size > 0:
print(f"Frame: {frame_num}, Ch: {ch}, Data Shape: {data.shape}")
else:
print(f"Frame: {frame_num}, Ch: {ch}, No audio data")
# デバッグ表示
cv2.imshow("Video", image)
if cv2.waitKey(1) == 27:
break
cap.release()
cv2.destroyAllWindows()
Windows(MSMF)だと、なぜか途中からAudioが取得できなくなる。。。
途中で1フレーム分の1470ポイントが取得出来ているので、コレちゃんと動くと便利な場面ありそう。
[ERROR:0@0.000] global cap_ffmpeg_impl.hpp:1124 open VIDEOIO/FFMPEG: unsupported parameters in .open(), see logger INFO channel for details. Bailout
Backend: MSMF
FPS: 30.0, Samples per Frame: 1470
Audio channels: 2
Audio sampling rate: 44100 Hz
Audio base index: 1
Frame: 1, Ch: 0, Data Shape: (1, 7320)
Frame: 1, Ch: 1, Data Shape: (1, 7320)
Frame: 2, Ch: 0, Data Shape: (1, 1470)
Frame: 2, Ch: 1, Data Shape: (1, 1470)
Frame: 3, Ch: 0, Data Shape: (1, 1470)
Frame: 3, Ch: 1, Data Shape: (1, 1470)
Frame: 4, Ch: 0, Data Shape: (1, 1470)
Frame: 4, Ch: 1, Data Shape: (1, 1470)
Frame: 5, Ch: 0, No audio data
Frame: 5, Ch: 1, No audio data
Frame: 6, Ch: 0, No audio data
Frame: 6, Ch: 1, No audio data
Frame: 7, Ch: 0, No audio data
Frame: 7, Ch: 1, No audio data
Frame: 8, Ch: 0, No audio data
Frame: 8, Ch: 1, No audio data
Frame: 9, Ch: 0, No audio data
Frame: 9, Ch: 1, No audio data
Frame: 10, Ch: 0, No audio data
Frame: 10, Ch: 1, No audio data
Ubuntu(GStreamer)だとGStreamer backend supports audio-only or video-only capturing.と怒られてそもそも動かない。
[ERROR:0@0.100] global ./modules/videoio/src/cap_ffmpeg_impl.hpp (1086) open VIDEOIO/FFMPEG: unsupported parameters in .open(), see logger INFO channel for details. Bailout
[ERROR:0@0.108] global ./modules/videoio/src/cap_gstreamer.cpp (1073) open GStreamer backend supports audio-only or video-only capturing. Only one of the properties CAP_PROP_AUDIO_STREAM=0 and CAP_PROP_VIDEO_STREAM=0 should be >= 0 [ WARN:0@0.108] global ./modules/videoio/src/cap_gstreamer.cpp (862) isPipelinePlaying OpenCV | GStreamer warning: GStreamer: pipeline have not been created [ERROR:0@0.162] global ./modules/videoio/src/cap.cpp (164) open VIDEOIO(CV_IMAGES): raised OpenCV exception: OpenCV(4.6.0) ./modules/videoio/src/backend_static.cpp:26: error: (-213:The function/feature is not implemented) VIDEOIO: Failed to apply invalid or unsupported parameter: [61]=5 / 5 / 0x00000005 in function 'applyParametersFallback'
まとめ
現時点の OpenCV のオーディオIO は
- 音声取得自体は可能だが、取得単位はバックエンド依存でフレーム同期されない
- 実用するには(使用用途にもよるが)明示的なバッファリングが必須
- 1つの VideoCapture で映像+音声を扱うのは、実質的に不可
という感じですかねー。
以上。