見出し画像

【DavinciResolve】配置間隔を指定可能な音声ファイル一括挿入スクリプト

今日もDavinciResolveのスクリプトをやっています。火注ゆかなです。
そろそろVRやりたいけれど、お休みが少なくて体力も時間もなかなか厳しい今日この頃です。おのれコロナめ。

今回は音声ファイルの一括挿入スクリプトを組んだので、その紹介です。


音声ファイル一括挿入スクリプトについて

今回組んだものの説明

……といっても、特定のフォルダ内のファイルを一括挿入するスクリプト自体は別段難しいものではありません。
DaVinci Resolve API Document v18 にも載っていますが、MediaPool:AppendToTimeline()メソッドでじゃんじゃか追加していけば良いだけです。

MediaPool:AppendToTimeline()はカレントタイムラインの中で一番下の映像・音声トラックへクリップを追加していくメソッドです。
単純にフォルダ内のメディアファイルを全部タイムラインに置きたい、という場合はこのメソッドを繰り返し実行すれば良いことになります。

ただ、どうしようもない欠点として、Timeline:InsertTitleIntoTimeline()のように時間を指定して挿入することはできません。
どう頑張っても「タイムラインの最後尾に追加する」ことしかできません。
使い勝手が悪すぎる。
(2024/1/11追記)
2023年の5月頃のアップデートで、MediaPool:AppendToTimeline()は指定の再生時間へ追加できるようになりました。
後日、修正版スクリプトを別の記事で紹介する予定です。

そうなると例えば、「タイムライン上の字幕のタイミングに合わせて音声ファイルを一括挿入」というようなことはできません。そもそもそういう需要があるかはさておき。
音声ファイルを望んだタイミングで再生されるようにするには手動で位置調整が必要です。

で、今回組んだのは、まさしく字幕のタイミングに合わせて音声ファイルを一括挿入する手助けをするスクリプトです。
百聞は一見に如かずということで、動かすとこんな感じ。

何をやっているのか?

おおざっぱに処理を分けると以下の通り。

  1. 挿入する音声ファイルのあるフォルダを指定

  2. カレントタイムラインから音声ファイルの配置タイミングを参照するトラックを指定

  3. ↑で指定したトラックに配置されたクリップの開始位置の一覧取得

  4. 作業用タイムライン(TL)と隙間埋め用タイムライン(TL)を作成

  5.  2.で取得したクリップの開始位置に来るまで、作業用TLに隙間埋めTLを配置して埋める

  6.  2.で取得したクリップの開始位置まで埋まったら作業用TLに音声ファイルを挿入

  7. 音声ファイル全部を挿入し終わるまで4.~5.を繰り返す

タイムラインの最後にしか追加できないなら、適切なタイミングまで空のタイムラインで埋めてやれば良いじゃないと。
配置タイミングの参照先は字幕だけでなく、映像や音声トラックも対象にできます。
なお、参照先トラックに配置されたクリップが音声ファイルより少ない場合、残りの音声ファイルは連続で隙間なく配置されます。

最後に一括挿入した音声ファイルを作業用タイムラインから目的のタイムライントラックへ手動コピペする作業は残ってます。


スクリプトの中身

スクリプトの中身はこんな感じです。
読みにくかったらごめんなさい。


--[[
   
   name: getWorkFolder
   @param メディアプール
   @return 作業用フォルダ
   
   作業用フォルダを検索して返却します。
   作業用フォルダが見つからない場合、ルートフォルダ直下に新規フォルダを作成して返却します
--]]

function getWorkFolder(mediapool)

    local rootfolder = mediapool:GetRootFolder()    --ルートフォルダ取得
    local folderlist = rootfolder:GetSubFolderList()    -- ルートフォルダ直下のフォルダ一覧取得

    -- 作業用フォルダを検索して返却
    for i=1, #folderlist do
        if folderlist[i]:GetName() == "VoiceInputWorkBin" then
            print([[作業用フォルダ "VoiceInputWorkBin" を取得しました]])
            return folderlist[i]
        end
    end
    
    local newfolder = mediapool:AddSubFolder(rootfolder, "VoiceInputWorkBin")    -- 作業用フォルダをルートフォルダの下に新規作成
    print([[ルードフォルダ直下へ作業用フォルダ "VoiceInputWorkBin" を作成しました]])
    return newfolder
end

--[[
   
   name: getWorkTimeline
   @param メディアプール、検索フォルダ
   @return 作業用TL、隙間埋め用TL、隙間埋め用TLクリップ(MediaPoolItem)
   
--]]
function getWorkTimeline(mediapool, folder)
    local cliplist = folder:GetClipList()    -- クリップ一覧取得
    local work_flg = false
    local fill_flg = false
    local workTL = nil    -- 作業用タイムライン
    local fillTL = nil        -- 隙間埋め用タイムライン
    local fillTLClip     = nil    -- 隙間埋め用タイムライン(クリップ) 
    
    -- 作業用タイムラインか、隙間埋め用タイムラインを検索して削除
    for i=1, #cliplist do
        if cliplist[i]:GetName() == "WorkTimeline" or  cliplist[i]:GetName() == "FillTimeline" then
            mediapool:DeleteClips({cliplist[i]})
        end
    end
    
    -- タイムライン作り直し
    local c_project = resolve:GetProjectManager():GetCurrentProject()
    local currentTL = c_project:GetCurrentTimeline()    -- 現在のタイムラインを一時保存
    
    mediapool:SetCurrentFolder(folder)
    workTL = mediapool:CreateEmptyTimeline("WorkTimeline") 
    fillTL = mediapool:CreateEmptyTimeline("FillTimeline") 
    fillTL:InsertFusionTitleIntoTimeline("TextPlus")        --何かTimelineアイテムがないとタイムラインの長さが0となり、AppendToTimelineでフレーム指定できないので適当にText+テンプレートを配置
    print("作業用タイムラインを作成し直しました")
    
    -- 隙間埋め用タイムラインをクリップとして取得し直す
    cliplist = folder:GetClipList()    -- クリップ一覧取得
    for i=1, #cliplist do
        if cliplist[i]:GetName() == "FillTimeline" then
            fillTLClip = cliplist[i]
        end
    end
    
    project:SetCurrentTimeline(currentTL)        -- カレントタイムラインを元に戻す
    return workTL, fillTL, fillTLClip
end


--[[
   
   name: importAudio2WorkTL
   @param
   @return
   
   ダイアログで指定したフォルダ内の音声ファイルを作業用TLへ一括挿入します。
   コピーして、目的のタイムラインの音声トラックへ貼り付けて使ってください
--]]
function importAudio2WorkTL()
    
    -- 現在のタイムラインを一時保存
    local project = resolve:GetProjectManager():GetCurrentProject()
    local currentTL = project:GetCurrentTimeline()
    local mediapool = project:GetMediaPool()
    -- カレントタイムラインのチェック
    if currentTL:GetName() == "WorkTimeline" or currentTL:GetName() == "FillTimeline" then
        print("作業用タイムラインがアクティブになっています。音声ファイルの配置タイミングを参照するタイムラインをアクティブにしてください。")
        return
    end

    -- 作業用フォルダ準備
    local workfolder = getWorkFolder(mediapool)                                -- 作業用フォルダ取得
    local workTL, fillTL, fillTLClip = getWorkTimeline(mediapool, workfolder)    -- 作業用TLの再作成と取得

     -- ダイアログ設定と表示
     local d = {}    -- ダイアログへ表示するコントロール一覧
     d[#d+1] = {"Path", Name="音声ファイルのフォルダパス", "PathBrowse"}
     d[#d+1] = {"Track", Name="トラックの種類", "Dropdown", Options={"Video(映像)", "Subtitle(字幕)", "Audio(音声)"}, Default=0}
     d[#d+1] = {"Ch", Name="トラック番号", "Text", Default="1", Lines=1}
     comp = fillTL:GetItemListInTrack("video", 1)[1]:GetFusionCompByIndex(1)    -- AskUser表示用にComposition取得
     local dialog = comp:AskUser("一括配置のタイミング参照タイムラインChの指定", d)  -- ダイアログ表示
     
     -- ダイアログ選択後の処理
     if dialog == nil then    -- キャンセルされた場合
        print("ダイアログをキャンセルしました")
     else
        local track_typeList = {"video", "subtitle", "audio"}
        local track_type = track_typeList[dialog.Track+1]    -- トラックの種類を対応する文字列へ変換
        local ch = tonumber(dialog.Ch)    -- トラック番号を数値に変換
        print("参照トラック番号:" .. track_type .. " [" .. tostring(ch) ..  "]")
        print(currentTL:GetTrackCount(track_type))
        -- ダイアログ入力値の確認
        if ch == nil then    -- トラック番号に数字以外が含まれる場合
            print("トラック番号が整数に変換できません。数値以外の文字が含まれていないか確認してください。処理を中断します。")
            return
        elseif currentTL:GetTrackCount(track_type) == nil then
            print("音声ファイルの配置タイミング参照先タイムラインにクリップが一つも設定されていません。\nタイミング参照用のクリップを配置するか、別のタイムラインを指定してください。")
            return
        elseif ch < 1 or currentTL:GetTrackCount(track_type) < ch then    -- トラック番号の指定値が選択できない数値の場合
            print("トラック番号が0以下か、タイムラインの最大トラック数を超えています。処理を中断します。")
            return
        end
        
        -- ダイアログで指定したフォルダ内のメディアファイル一覧取得
        local mediastorage = resolve:GetMediaStorage()    --メディアストレージ取得
        local mediapathList = mediastorage:GetFileList(dialog.Path)    -- ファイル一覧取得(このメソッドでは映像や音声ファイルのみが対象となる)
        table.sort(mediapathList)    --    ソート
        print("フォルダ内のメディアファイル一覧")
        dump(mediapathList)

        -- GetFileList()ではmp4などの動画ファイルも対象になるため、wavファイルのみ抽出
        local audioFolder = mediapool:AddSubFolder(workfolder, os.date("%Y%m%d_%H%M%S"))    --音声ファイルインポート用フォルダを作成(フォルダ名は処理実行日時)
        local audiopathList = {}    
        for i=1, #mediapathList do    -- wavファイルのみ抽出
            if mediapathList[i]:find([[%.wav$]]) then    -- パス名が「.wav」で終わるファイルを対象
                audiopathList[#audiopathList+1] = mediapathList[i]
            end
        end
        dump(audiopathList)
        -- 音声ファイルインポート、及びクリップ一覧取得
        mediapool:SetCurrentFolder(audioFolder)
        local audioList = mediapool:ImportMedia(audiopathList)    
        -- 音声クリップ一覧をファイル名でソート
        table.sort(audioList,
            function(a,b)
                return (a:GetName() < b:GetName())
            end)

        local tlItemList = currentTL:GetItemListInTrack(track_type, ch)            -- 音声ファイルの配置タイミング参照TLアイテムリスト取得
        if #tlItemList <= 0 then
            print("指定されたトラックにクリップが配置されていません。\nタイミング参照用のクリップを配置するか、別のトラックやタイムラインをを指定してください")
            return
        end
        print("参照TLアイテムリスト:" .. #tlItemList)
        dump(tlItemList)
        print("音声ファイルリスト:" .. #audioList)
        dump(audioList)
        
        project:SetCurrentTimeline(workTL)        -- 作業用カレントタイムラインへ移動
        -- 音声ファイルの配置処理開始
        -- タイミング参照用のクリップが音声ファイルより少ない場合、残りの音声ファイルは連続で配置
        for i=1, #audioList do
            if i <= #tlItemList then
                local start_frame = tlItemList[i]:GetStart()    -- 目安TLアイテムの開始フレーム取得
                local frame = workTL:GetEndFrame()    -- 作業用TLの再生終了フレームを初期値に設定
                if 2 <= i then    -- なぜか2つ目以降の音声ファイルが1フレーム後ろにズレるので補正
                    frame = frame + 1
                end 

                -- 次の配置タイミングまで5秒以上空く場合、5秒以下になるまで隙間埋めTL配置
                local full_fill_cnt = math.floor((start_frame-frame) / fillTL:GetEndFrame())    -- 5秒分の隙間埋め回数
                if 0 < full_fill_cnt then
                    local fill_table = {}
                    for c=1, math.floor((start_frame-frame) / fillTL:GetEndFrame())  do
                        fill_table[#fill_table+1] = fillTLClip
                    end
                    mediapool:AppendToTimeline(fill_table)    -- 隙間埋め用TLを5秒分配置
                end
                -- 5秒以下の隙間を埋める
                local fill_len = (start_frame-frame) % fillTL:GetEndFrame() -1
                if 0 <= fill_len then
                    local fill_r_table = {}
                    fill_r_table["mediaPoolItem"] = fillTLClip
                    fill_r_table["startFrame"] = 0
                    fill_r_table["endFrame"] =  fill_len
                    mediapool:AppendToTimeline({fill_r_table})    -- 隙間埋め用TLを音声ファイル配置タイミング-1フレーム分まで配置
                end
                print("隙間埋めTL配置")
                
            end
            
            mediapool:AppendToTimeline(audioList[i])    -- 隙間埋め用TLを120フレーム分配置
            print("音声ファイル挿入:" .. audioList[i]:GetName())
        end
        
     end
end


-- 取り込み処理の実行開始
importAudio2WorkTL()

ダイアログはAskUserメソッドを利用しました。
Compositionがないと呼び出せないのでEditページから呼び出すにはやや不便ですが、簡素なUIで良いならこっちの方がデザインは楽ですね。

fusion.UIManagerの方がCompositionの制限なく呼び出せますし色々細かく作り込めますが、指定しなくちゃいけない値も多いので簡単なUIを作ろうとすると逆に面倒です。
AskUserメソッドについてはFusion8_Scripting_Guide.pdfの43ページ以降を参照してください。


スクリプトファイル

一応、スクリプトファイルを置いておきます。
「C:\ProgramData\Blackmagic Design\DaVinci Resolve\Fusion\Scripts\Comp\」の下に置くとアプリの方から選択できるようになります。

スクリプトを動かす際は、
・1トラックに挿入したい音声ファイルを一つのフォルダにまとめる
・Editページで音声ファイルを挿入したいタイムラインをアクティブ状態にしておく
上記2つを満たしてうえでスクリプトを実行してください。

上手く動かない場合はコンソールを開いてみてください。簡単なエラー表示などはコンソールに出力するようにしてます。
あと4ファイルくらいでしかテストしていないので、音声ファイルが凄く多い場合の配置速度に関しては試してないのでわからないです。
もしかしたら不具合出るかも。その時はTwitterの方にでもDMとか下されば対応できるかもしれません。


そもそもこんなスクリプトに需要あるのかなあと思いつつ、とりあえず頑張って作ったので記事として投げてはおきます。
お役に立てば幸いです……これは本当に役に立つんですかね?

サポートしていただけるとその分の価値を提供できてるんだなって励みになります。