見出し画像

【DavinciResolve API】UITimerウィジェットの使い方

まえがき

新年明けましておめでとうございます。
火注ゆかなです。

この記事を読んでいる人たちも年末年始は帰省された方、のんびりされた方、年始も休まず働いていた方など様々な過ごし方をされているかと思います。

私は年末年始も普通にお仕事でした。世間と同じタイミングで長期休暇を取りにくいのはシフト勤務の辛いところですね。冬休みはもうちょっと先です。

さて、年始のお仕事を終えてようやく取れたお休みですが、何故か新年早々Davinci Resolve Scriptingのお話を投稿しております。

今は指定ファイルの変更を検知するまでの待機処理を作成中なのですが、LuaにはSleep関数などがなくて困りました。
os.clock()やos.date()を利用すれば、指定時間経過までループ処理を回して待機することもできますが、地味にCPU負荷が上がるのが嫌なんですよね。

DavinciResolveにはwait(second)という関数もあります。が、こちらは秒単位でしか指定できないので多用すると処理がガクッと遅くなってしまいます。
(後ほど気づいたのですが、普通に小数点以下も指定できました。os.clockを使って測定したせいか10ms前後の誤差はありましたが、wait(0.01)というように10ms単位でのスリープ程度なら問題なく使えそうです)

何か便利な関数はないかなあと色々調べていたところ、どうやらUIにTimerウィジェットなるものが含まれているという情報を発見。
結論から言うと私が実装したかった処理にはあまり関係がなかったのですが、折角調べたのでまとめておくことにしました。

ちなみに先に言っておきますが、Pythonでスクリプト組んでる人はPyQtとかPySideのようなGUI作成ツールあると聞きますし、そちらでGUI組めるならその方が良いと思います。今回のUITimerもQTimerの互換品ですし。

DavinciResolveのスクリプトをLuaで組んでいる私のような人間としてはGUI組む際の選択肢がほぼありませんが、DavinciResolve API のGUIはQtの劣化版っぽいので色々と不便なので。



UITimerの仕様

DavinciResolve APIのGUIはQtと呼ばれるフレームワークがベースだそうです。
使い方は若干違いますがUITimerもQtimerとほぼ同じようで、UITimerのメンバーについては下記のページを参考にさせていただきました。

UITimerのメンバー

Interval(int):タイマー開始してからTimeoutイベントが発生するまでの間隔。単位はミリ秒(ms)
               例えば5000を指定すれば5秒間隔になる。

SingleShot(boolean):タイマーを開始すると、停止するまではIntervalに設定した間隔で
                     何度もTimeoutイベントが繰り返し発生する。
                     SingleShotを有効にするとTimeoutが1回発生したらタイマーが止まる。

TimerType(String):タイマーの精度。3種類設定可能で、デフォルトはCoarseTimer。
                   以下の3種類以外の文字列を設定した場合もCoarseTimerになる。
    ・PreciseTimer   :精度が高い。1msもズレて欲しくないならこちら。ただし消費電力も上がる模様?
    ・CoarseTimer    :精度がやや粗い。最終的に5%程度(5ms以下?)の誤差が生じる模様?
    ・VeryCoarseTimer:精度が粗い。Intervalが最も近い秒単位(1000ms単位)に丸められる?

RemainingTime(int):タイマーが0になるまでの残り時間。
                    0を下回った場合、再度Intervalの時間からカウントダウンする。
                    SingleShotが有効だと0を下回っても−1のままとなる。

IsActive(boolean):タイマーが起動しているかどうか。読み取り専用。

TimerID(int):タイマーが実行される度に自動で割り振られるID。読み取り専用。
              タイマーが実行中なら割り振られたID、停止中なら-1を保持。

UITimerのメソッド

SetInterval(int):Interval(タイマー間隔)を設定する。
GetInterval():Intervalを返却する。

SetSingleShot(boolean):SingleShot(一度きり設定)を設定する。
GetSingleShot():SingleShotを返却する。

GetRemainingTime():RemainingTime(残り時間)を返却する。

GetIsActive():タイマーのアクティブ状態を返却する。

GetTimerID():タイマーIDを返却する。タイマーが停止中なら-1を返す。

SetTimerType(String):TimerTypeを設定する。
GetTimerType():TimerTypeを返却する。

Start():タイマーを開始する。
Stop():タイマーを停止する。SingleShotが有効な場合、残り時間が0になった時点で勝手に停止する。

基本的な使い方

  1. UITimerを適当な変数に格納

  2. UI DispatcherにTimeoutイベントハンドラを追加

  3. UITimer:Start()でタイマーを起動する

  4. 適切なタイミングでUITimer:Stop()でタイマーを停止する

他のGUIウィジェットと異なり、UITimerはウィンドウの子要素としてではなく個別の変数で管理することになります。表示するようなものではないので当然といえば当然ですね。
また、TimeoutイベントもUI Dispatcherに記述します。

ボタンを押したらタイマー処理を行うコードはこんな感じになります。

ui = fu.UIManager
disp = bmd.UIDispatcher(ui)
local width,height = 300,200        -- 初期ウィンドウサイズ

 -- ウィンドウのUI配置設定
win = disp:AddWindow({
    ID = 'MyWin',
    WindowTitle = 'テストウィンドウ',
    Geometry = { 100, 100, width, height },
    Spacing = 10,
    
    ui:VGroup{
        ui:HGroup{
            -- タイマー処理を実行するボタン
            ui:Button{
                ID = "ButtonTimerStart", 
                Text = "タイマー実行" 
            }
        }
    }
 })
local itm = win:GetItems()

 
 -- タイマー設定
local timer = ui:Timer{
            ID = "MyTimer",       -- ID。イベントハンドラではev.whoで参照可能
            Interval = 5000,      -- 実行間隔(今回は5秒設定)
            SingleShot = false    -- タイムアウト処理は1回だけにするか
        }        
print("タイマータイプ:" .. timer:GetTimerType())
 
 
-- イベントハンドラ:ボタンクリック時
function win.On.ButtonTimerStart.Clicked(ev)
    
    print("タイマーアクティブ状態:" .. tostring(timer:GetIsActive()))
    print("タイマーID:" .. tostring(timer:GetTimerID()))

    print("処理開始時刻:" .. os.date())
    
    timer:Start()    -- タイマースタート

    print("タイマーアクティブ状態:" .. tostring(timer:GetIsActive()))
    print("タイマーID:" .. tostring(timer:GetTimerID()))
    
    print("残り時間:" .. timer:GetRemainingTime() .. "(ms)")
    
    wait(1)    -- 1秒Wait
    print("残り時間:" .. timer:GetRemainingTime() .. "(ms)")
    
    print("処理終了時刻:" .. os.date())
end


-- イベントハンドラ:タイムアウト
function disp.On.Timeout(ev)
    print("タイムアウトしました:" .. os.date())
    timer:Stop()    -- タイマーストップ
end
 
 -- クローズハンドラ:ウィンドウ右上の✕ボタンクリック時
function win.On.MyWin.Close(ev)
    disp:ExitLoop()
end


-- ウィンドウ表示・操作受付開始
win:Show()
disp:RunLoop()

win:Hide()
実行結果

注意点

UITimerは並列処理をするものではありません。一定間隔でTimeoutイベントをイベントキューに追加するだけなので、wait()関数で停止している間はTimeoutイベント処理が実行されません。

もしInterval設定時間が5秒の時にwait()関数で10秒ほど停止した場合、Timeoutイベントはどうなるでしょうか?

次のコードで確認してみます。
Timeoutイベント内でUITimer:Stop()を実行しているため、本来ならTimeoutイベントは1回処理されたらタイマー停止→2回目以降のイベントは発生しないはずです。

-- wait()で10秒以上待機した場合のTimer動作確認コード

local timer = ui:Timer{
            ID = "MyTimer",       -- ID。イベントハンドラではev.whoで参照可能
            Interval = 5000,      -- 実行間隔
            SingleShot = false    -- タイムアウト処理は1回だけにするか
        }
        
print("タイマータイプ:" .. timer:GetTimerType())
 
-- イベントハンドラ:ボタンクリック時
function win.On.ButtonTimerStart.Clicked(ev)
    
    print("タイマーアクティブ状態:" .. tostring(timer:GetIsActive()))
    print("タイマーID:" .. tostring(timer:GetTimerID()))

    print("処理開始時刻:" .. os.date())
    
    timer:Start()    -- タイマースタート

    print("タイマーアクティブ状態:" .. tostring(timer:GetIsActive()))
    print("タイマーID:" .. tostring(timer:GetTimerID()))
    
    print("残り時間:" .. timer:GetRemainingTime() .. "(ms)")
    
    wait(1)    -- 1秒Wait
    print("残り時間:" .. timer:GetRemainingTime() .. "(ms)")
    
    wait(5)    -- 5秒Wait
    print("残り時間:" .. timer:GetRemainingTime() .. "(ms)")
    
    wait(5)    -- 5秒Wait
    print("残り時間:" .. timer:GetRemainingTime() .. "(ms)")
    
    print("処理終了時刻:" .. os.date())
end


-- イベントハンドラ:タイムアウト
function disp.On.Timeout(ev)
    print("タイムアウトしました:" .. os.date())
    timer:Stop()    -- タイマーストップ
end
実行結果:タイムアウト処理が2回まとめて実行されている

ボタンクリックイベント終了後にTimeoutイベントが2回連続で実行されました。
Timeoutイベント処理自体は先に実行されているボタンクリックイベントの後じゃないと実行されないため、Interval設定時間を過ぎてもタイマーは停止しません。
一方でwait()関数でメイン処理が待機している間もタイマーは動き続けてTimeoutイベントを蓄積していきます。

タイマー自体は非同期でカウントされるものの、Timeoutイベントは他のイベントと同じようにシングルスレッドで順番に処理されます。
重い処理や入力待機処理、Sleep処理等がある場合はTimeoutイベント処理のタイミングがズレる可能性がある点には注意が必要です。

もしTimeoutイベントが連続で発生すると困るというのであれば、SingleShotを有効化しておくと良いでしょう。
UITimer:Stop()を実行しなくてもInterval設定時間を過ぎると自動的にタイマーを停止してくれます。

SingleShotを有効化した状態で実行すると以下のようになります。

SingleShotを有効化した場合の実行結果

Intervalの2回分の時間が経過していてもTimeoutイベントは1回分しか処理されてないことがわかります。ちゃんとタイマーが停止していますね。
ちなみにタイマー開始してから5秒以上経過後にGetRemainingTime()で取得した残り時間は-1になるようです。



Timerの使い道

定期的に画面やデータを更新する、画像を高速で切り替えてアニメーションするなどが主な使い道になるようです。

とはいえ、動画編集ソフトのGUIでわざわざアニメーションさせる必要があるかは微妙なところですね。
他にパッと思いつくところだと、DavinciResolveだとタイムラインとGUIで同期を取ったり、フォルダを監視してメディアプールとデータを同期するといった使い道があるかもしれません。

例えば、GetCurrentTimeCode()で取得した内容をラベルに反映するような処理を記述しておけば、タイムライン再生中でもリアルタイムでGUI側に表示できそうですね。あとはクリップ情報とか。
DavinciResolve APIは編集を自動化するには機能が足りなくて結局手作業が必要になるため、タイムラインを手動操作した結果を同期できるのは便利かと思います。

それから、フォルダ監視機能は結構使いどころが多そうですね。
GUIのボタンを押すとフォルダ監視を開始して、新しいファイルが追加されたらメディアプールへ自動で追加するのは普通に便利そうです。
再度ボタンを押すか、GUIウィンドウを閉じると監視を終了すればシンプルに実装できそう。良いですねこれ。

他には処理の進捗を表すプログレスバーの実装とかでしょうか。
やっぱり時間がかかる処理がいつ終わるのか確認できるのって大事ですよ。

色々と実装していくと、一つのスクリプトの中で複数のタイマーを同時に実行することもでてくるかと思います。
この場合、ちゃんとタイマーを定義する際にID属性を設定しておけば、Timeoutイベントハンドラでev.who属性を参照することでどのタイマーから呼び出されたのか判別することが出来ます。

そうなると、IDで条件分岐するよりは、IDをキーに関数を格納したテーブルを作成するとスマートなんですかね。
実装するとしたらこんな感じでしょうか。

timer_1 = ui:Timer{
    ID = "MyTimer_1",   -- ID
    Interval = 5000,    -- 実行間隔
    SingleShot = true,  -- タイムアウト処理は1回だけにするか
}
timer_2 = ui:Timer{
    ID = "MyTimer_2",   -- ID
    Interval = 1000,    -- 実行間隔
    SingleShot = true,  -- タイムアウト処理は1回だけにするか
}

timer_func["ID"] = function()
     -- タイマー毎に合わせた処理を記述
end

-- ~関数設定省略~

-- イベントハンドラ:タイムアウト
function disp.On.Timeout(ev)
    timer_func[ev.who]()    -- タイマーのID毎に設定された関数実行
end

あとはLuaだと1ms単位のタイマーとして使えるでしょうか。
TimerTypeにPreciseTimerを指定して精度を上げて、SingleShotを有効化してタイマーを開始しておき、GetRemainingTime()で返却された残り時間が-1か判定しながらループを回すとか。

でもループ文回して待機する方法は結局はCPU負荷が増えますし、それならos.clock()で同じようなことが出来るのでわざわざタイマーとして使う必要があるかというと、うーん……。

まあ、ともかくそんな感じで色々便利そうだよということで。


あとがき

今回は本当に苦労しました。
UITimerのまともな使い方についての情報が全然見つからないんですよ。we suck lessのフォーラムも検索に引っかかりましたが使えるということしかよくわからない。

最終的にはResolveDevDocに記述されているコード例を頼りに試行錯誤して使い方が判明しましたが、そのコード例だってちょっとよくわからなかったですし……あれは編集者もどこかから丸々コピーしただけで試していませんね?

BMDの方々にはもうちょっとこう……スクリプト機能についてもちゃんと情報を公開してほしいです。未開示情報が多すぎて困ります。


今回は大分長くなってしまいました。
この情報が皆様のお役に立てば幸いです。

それでは今年も一年頑張りましょう!

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