0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Swift/macOSでlibmpvをヘッドレス実行して動画からWAV音声を抽出する

0
Last updated at Posted at 2026-07-05

macOSアプリで動画から文字起こし用の音声を取り出す必要があり、libmpv をヘッドレス実行してWAVを書き出す実装を入れました。

この記事では、動画プレーヤーアプリで使っている実装から、音声抽出部分だけを切り出します。

やることは次の1点です。

  • ローカル動画を libmpv で開き、16kHz mono WAVとして一時ファイルに書き出す

Video Notes pipeline

なぜAVFoundationではなくlibmpvを使うか

mp4 / mov だけを相手にするなら、AVAssetReader などで処理できます。

ただ、動画プレーヤー側では mkv / webm など、AVFoundationがそのまま開けないコンテナも扱いたいケースがあります。文字起こし用の音声抽出だけAVFoundationに寄せると、

  • 再生はできるのに音声抽出はできない
  • 形式ごとに別の抽出経路を持つ必要がある

という状態になります。

そこで、再生にも使っている libmpv を短命のヘッドレスインスタンスとして起動し、ao=pcm でWAVを書き出す形にしました。

mpvの設定

音声だけ取り出すので、映像出力は完全に止めます。

option 目的
terminal no ターミナル出力を抑える
config no ユーザーのmpv設定を読まない
msg-level all=no ログを抑える
vid no 映像をデコードしない
vo null 映像出力を持たない
ao pcm 音声をPCMファイルに出す
ao-pcm-file 出力先path WAVの出力先
ao-pcm-waveheader yes WAVヘッダを付ける
audio-samplerate 16000 文字起こし向けに16kHzへ
audio-channels mono monoへ

vid=novo=null を入れているのは、動画を表示したいわけではないからです。UI用の再生とは別に、音声抽出専用のmpvを作ってすぐ破棄します。

実装

実装は次のようにしています。

import Foundation
import Libmpv

enum AudioExtractor {
    nonisolated static func extractAudio(
        from url: URL,
        startSec: Double? = nil,
        lengthSec: Double? = nil
    ) -> URL? {
        guard let h = mpv_create() else { return nil }

        let out = FileManager.default.temporaryDirectory
            .appendingPathComponent("reel-notes-\(UUID().uuidString).wav")

        func opt(_ key: String, _ value: String) {
            _ = mpv_set_option_string(h, key, value)
        }

        opt("terminal", "no")
        opt("config", "no")
        opt("msg-level", "all=no")
        opt("vid", "no")
        opt("vo", "null")
        opt("ao", "pcm")
        opt("ao-pcm-file", out.path)
        opt("ao-pcm-waveheader", "yes")
        opt("audio-samplerate", "16000")
        opt("audio-channels", "mono")

        if let startSec {
            opt("start", String(startSec))
        }
        if let lengthSec {
            opt("length", String(lengthSec))
        }

        guard mpv_initialize(h) >= 0 else {
            mpv_terminate_destroy(h)
            return nil
        }

        let args = ["loadfile", url.path]
        let dup = args.map { strdup($0) }
        defer { dup.forEach { free($0) } }

        var cargs: [UnsafePointer<CChar>?] = dup.map { UnsafePointer($0) } + [nil]
        cargs.withUnsafeMutableBufferPointer {
            _ = mpv_command(h, $0.baseAddress)
        }

        let deadline = Date().addingTimeInterval(600)

        loop: while Date() < deadline {
            guard let event = mpv_wait_event(h, 1.0) else {
                break
            }

            switch event.pointee.event_id {
            case MPV_EVENT_END_FILE, MPV_EVENT_SHUTDOWN:
                break loop
            default:
                continue
            }
        }

        mpv_terminate_destroy(h)

        guard FileManager.default.fileExists(atPath: out.path),
              let size = try? FileManager.default
                .attributesOfItem(atPath: out.path)[.size] as? Int,
              size > 1024 else {
            return nil
        }

        return out
    }
}

mpv_wait_eventMPV_EVENT_END_FILE を待つと、WAVの書き出し完了を検知できます。

ただし、異常な入力でイベントが返らないとタスクが終わらなくなるので、実装では600秒の上限を入れています。

UIスレッドで実行しない

この処理は mpv_wait_event を回すブロッキング処理です。

SwiftUIアプリのメインアクターで直接呼ぶとUIが止まるので、呼び出し側では Task.detached に逃がしています。

let wav = await Task.detached(priority: .userInitiated) {
    AudioExtractor.extractAudio(from: videoURL)
}.value

抽出できたWAVは文字起こしに渡し、使い終わったら削除します。

if let wav {
    let segments = await transcribe(wav)
    try? FileManager.default.removeItem(at: wav)
}

部分抽出したい場合

長い動画の一部だけを処理したい場合は、startlength を渡せます。

let wav = AudioExtractor.extractAudio(
    from: url,
    startSec: 60,
    lengthSec: 120
)

この場合、60秒地点から120秒分だけをWAVにします。

はまった点

ao-pcm-waveheader=yes を忘れない

ao=pcm だけだと、後段のAPIが期待するWAVとして扱いにくくなります。AVAudioFile などで読む前提なら、ao-pcm-waveheader=yes を入れてWAVヘッダ付きにしておくのが安全です。

mpvのユーザー設定を読ませない

アプリ内処理として使うなら、ユーザーの mpv.conf に影響されると再現性が落ちます。

opt("config", "no")

を入れて、アプリ側で必要なオプションを明示します。

サンドボックス下ではURLの権限は別問題

この記事のコードは、渡された URL にアクセスできる前提です。

Mac App Store向けのサンドボックスアプリでは、事前に NSOpenPanel や security-scoped bookmark でアクセス権を確保してから、この抽出処理に渡す必要があります。

まとめ

libmpv を再生エンジンとして組み込んでいるアプリなら、音声抽出にも同じデコーダを使うと対応形式を揃えられます。

ポイントは次の3つです。

  • ヘッドレスな短命mpvインスタンスを作る
  • vid=no / vo=null / ao=pcm でWAVを書き出す
  • mpv_wait_event を使うのでメインアクターでは実行しない

動画プレーヤーや動画解析アプリで、mkv / webm も含めて音声を取り出したい場合には扱いやすい方法でした。


関連記事:


この実装は、macOS向け動画プレーヤー Reel のVideo Notes機能で使っています。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?