ユーニックス総合研究所

  • home
  • archives
  • pc-rec-soft

PCで使える録画ソフトの開発。補正で音ズレ解消【日記】

  • 作成日: 2022-06-14
  • 更新日: 2023-12-24
  • カテゴリ: 日記

PCで使える録画ソフトの開発

Youtubeに動画を投稿するようになって最初はフリーの録画ソフトを使っていた。
しかしそのソフトがフリーで使えなくなってしまった。

そのソフトを購入すればいいのだが予算がそんなに無い。
だから自分で録画ソフトを開発することにした。

だが録画ソフトの開発はけっこう難易度が高い。
開発してみると動画と音声がずれる現象が起こってしまった。
これを長らく解決できないでいたが、最近になって解決できたかもしれない、という感触を得た。

この記事ではその記録を残したいと思う。

録画ソフトの構成

いま現在、開発している録画ソフトはPythonで開発している。
どのような構成にしているか。

まず録画だが、これはスクリーンショットを撮ってその絵を繋げて動画にするようにしている。
Pythonではスクリーンショットはpyautoguiで撮れる。
さらに動画の生成にはcv2を使っている。
これでスクリーンショットを撮って動画ファイルに書き込む方法は↓である。

import cv2  
import pyautogui  
import numpy as np  

fourcc = cv2.VideoWriter_fourcc(*"XVID")  

writer = cv2.VideoWriter(  
    out_path,  
    fourcc,  
    FPS,  
    (width, height),  
)  

img = pyautogui.screenshot(region=(left, top, width, height))  
frame = np.array(img)  
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)  
writer.write(frame)  

上のコードでは細かい変数の定義は省略しているので変数名から察してほしい。

cv2で動画のエンコーディングをXVIDにしている。
そしてライターを作成する。
あとはpyautoguiで撮った画像をライターで書き込んでいけば動画にすることができる。

録画の基本ロジックはこれである。
いたってシンプルだ。

動画の録画ではスレッドを1つ立ててそのスレッドの中で行っている。
音声の録音はまた別のスレッドを立ててそのスレッドで行う。

動画と音声のズレ

この方法で録画と録音をするとまず動画と音声がずれる。
これは音声に比べて動画が短くなったり長くなったりすることでずれる。

このズレの修正にけっこう長い時間手間取っていた。

動画と音声は最終的にffmpegでマージして1本の動画ファイルにする。
このマージした動画で絵と音声がずれる。

ループ内でスリープすると動画が短くなる

スクリーンショットの撮影はループ文の中で行う。
録画ボタンが押されてループに入り、停止ボタンが押されるまでそのループの中でスクリーンショットを撮り続ける。

このループ内でtime.sleep()する必要がある。
FPSを計算し、そのFPSに合うようにスリープを行えば理屈の上ではズレはなくなる。
だが、実際はそんなことはない。

やはりわずかな誤差が蓄積されていくと最終的に動画の絵と音はずれることになる。
このズレを補正しなくてはいけない。

1フレームにかかる時間

FPSが10だとする。
そうすると1秒間に10枚の絵が必要になる。

ということは1枚にかかる時間は1/10で0.1秒である。

FPS = 10  
one_frame_time = 1 / FPS  

スクリーンショットを撮るのもタダではない。時間がかかる。
だからその処理にかかった時間を測定する。

そうしてそのかかった時間と0.1秒の差分がスリープタイムになる。
この時間だけスリープすれば1枚のスクリーンショットを0.1秒で撮れるわけである。

0.1秒が10回続けばそれは1秒であるから、ズレもなくなることになる。

だが実際にはこのような計算を行っても結果はズレる。
これはループ自体の処理とループ内のほかの処理の処理時間が蓄積され、その分長くなるからだと思われる。
だからこのズレをプログラム的に補正していかなければいけない。

FPSの誤差を計算する

まず最初に行ったのがFPSの誤差補正だ。
↓のようなコードを書いた。

count = 0  # スクリーンショットの数  
loop_start_time = time.time()  

for _ in range(nloop):  
    start_time = time.time()  

    # 色々な処理  
    count += 1  

    cur_time = time.time() - loop_start_time  
    now_fps = count / cur_time  
    diff_fps = FPS - now_fps  
    gosa_time = diff_fps * one_frame_time  

    calc_time = time.time() - start_time  
    wait_time = one_frame_time - calc_time  
    wait_time -= gosa_time  

    if wait_time > 0:  
        time.sleep(wait_time)  

now_fpsというのが現在のFPSである。
目標値は10FPSだが、実際の処理ではこれが8とか9とか11になったりする。

これを固定の期待するFPSと差分を取ってdiff_fpsとする。
diff_fpsone_frame_timeを掛け算すれば誤差時間が求まる。

あとはこの誤差をwait_timeから引けば誤差が補正される。

この方法はある程度の成果を上げた。
だが長い動画を撮るとやはりズレが発生した。
このズレについても補正が必要である。

経過時間のギャップを補正する

最近になって追加した処理が↓である。

    if count % 100 == 0:  
        # 一定間隔で時間のギャップを調整する  
        cur_time = time.time() - loop_start_time  # 現実の経過時間  
        hope_total_time = count * one_frame_time  # 期待する時間の論理値  
        if cur_time < hope_total_time:  
            # 論理値のほうが大きいならスリープして動画の時間を短くする  
            diff_time = hope_total_time - cur_time  
            time.sleep(diff_time)  
        elif cur_time > hope_total_time:  
            # 論理値の方が小さいのであればフレームを追加して動画を長くする  
            diff_time = cur_time - hope_total_time  
            n = int(diff_time / one_frame_time)  
            for _ in range(n):  
                writer.write(frame)  
                count += 1  

録画をはじめて実際に経過してる時間(cur_time)がある。
これが絶対的な現実世界の基準である。

そしてFPSと撮ったスクリーンショットの数から論理的な経過時間が求まる。
たとえば1枚に0.1秒(one_frame_time)使うとすると、論理的な経過時間はcount * one_frame_timeで求まる。

現実の経過時間とこの論理的な経過時間を表示してみると、現実の経過時間の方が長くなる。
これはFPS誤差補正のスリープの影響である。
FPS誤差の修正ではtime.sleep()しかしてないので、動画の時間は短くなるばかりである。
だから現実の時間のほうが長くなる。

現実の経過時間のほうが短ければ、これは録画時間が長いことになる。
つまりtime.sleep()でスクリーンショットの数を減らし、動画を短くすればいい。

現在の経過時間のほうが長ければこれは動画が短いということになる。
だからこの場合は最後に撮ったスクリーンショット(frame)を追加で書き込めばいい。
こうすることで動画のフレーム数が増えて動画の時間が伸びる。

この調整を追加で記述したところ、ギャップがほぼ0.1ぐらいに安定した。
これぐらいのギャップであればズレはほとんど目立たない。
ミッションコンプリートである。

おわりに

今回は録画ソフトで録画時間を補正する方法について記録を残した。
マルチスレッドで処理時間が異なってしまう場合は補正が必須になるだろう。

🦝 < 補正、補正、補正

🐭 < 補正は正義