銀月の符号

Python 使い見習いの日記・雑記

AviUtl を Python から自動操縦する、のか?

動画のエンコードの際、世話になっているソフト、 AviUtl。これを Python から操ってみようと思ったのがことの始まり。

とはいえ、便利なものがすでに世に存在している。 「AviUtl Control」 。作者であるあじさんに感謝。

「AviUtl Control」は AviUtl をコマンドラインから操作する exe 郡。

方針1 subprocess モジュール

ファイルを開くには auc_open.exe, 閉じるには auc_close.exe といったように対応する exe を呼べばいいのだから subprocess モジュールでたたく。それだけ。味付けとして docstring をつけてみたり。

pyauc.py

PyAviUtlControl ダウンロード

# coding: utf-8
u"""AviUtl を Python から操作する

AviUtl Control ver1.4 を使用
http://www.geocities.jp/aji_0/
"""

import subprocess

VERSION = '1.0'
AUC_VERSION = '1.4'

def _call_auc(exe, window=None, *args):
    u"""auc_*.exe を呼ぶショートカット関数"""
    command = [exe]
    command.extend(map(str, args))
    if window is not None:
        command.insert(1, (str(window)))
    subprocess.check_call(command)

def aviout(filename, window=None):
    u"""AVI 出力する
    
    出力の終了を待たずに返る。
    終了するまで待つには、この後に wait を呼べばよい。

    出力ファイルと同名のファイルがすでに存在し、
    上書き確認のダイアログが出てしまうと、止まってしまうので注意。
    「システムの設定」の「ファイル選択ダイアログで上書き確認をしない」
    を有効にしておくと回避できる。"""
    _call_auc('auc_aviout.exe', window, filename)

def close(window=None):
    u"""ファイルを閉じる"""
    _call_auc('auc_close.exe', window)

#略

def open(filename, window=None):
    u"""ファイルを開く"""
    _call_auc('auc_open.exe', window, filename)

#略

これでなんら問題ない、はずだった。しかし、「AviUtl Control」にはソースコードが付属している。ここに興味を持ってしまったのが間違いだった。 目的と手段が逆転し、非生産的な遊びの始まりへ。

方針2 ctypes モジュール

Windows API は ctypes を使えばたたける。これで道具はそろうので、あとはソースを参考に移植していけば Python だけで同じことができる。

メリット
  • Python 単体で解決している。あくまで「AviUtl Control」 を参考にして作った Python モジュールであり 「AviUtl Control」は不要。
デメリット
  • (コードを書く側のみの問題)Windows API 各種の定義をひとつひとつ MSDN でしらべて ctypes の prototype で定義する、という手間がかかる。
  • (コードを書く側のみの問題)WM_COMMAND などの windows.h に由来するマクロの具体的な値も知る必要がある。
  • (コードを書く側使う側双方の問題)「AviUtl Control」 がバージョンアップした際、追随するには Python コードの改変が必要。
pyaviutl.py (未完成)

現在作成中のコードはこんな感じ。 auc_close.c, auc_open.c の二つの移植だけ完了したところ。つまりファイルを開いたり閉じたりできるだけ。

# coding: utf-8
u"""AviUtl を Python から操作する

オリジナル
AviUtl Control ver1.4
http://www.geocities.jp/aji_0/
"""

import itertools
import time
from ctypes import windll, WINFUNCTYPE, WinError, cast
from ctypes import c_int, c_wchar_p, c_void_p
from ctypes.wintypes import BOOL, UINT, LONG, LPCWSTR, HWND, WPARAM, LPARAM

class AviUtlError(StandardError):
    pass

user32 = windll.LoadLibrary('user32.dll')

LPCTSTR = LPCWSTR
WM_COMMAND = 0x111
WM_SETTEXT = 0x0c
WM_LBUTTONDOWN = 0x201
WM_LBUTTONUP = 0x202
GWL_HWNDPARENT = -0x08

def _errcheck_null(result, func, args):
    if result is None:
        raise WinError()
    return args

def _errcheck_false(result, func, args):
    if not result:
        raise WinError()
    return args

FindWindow = WINFUNCTYPE(HWND, LPCTSTR, LPCTSTR)(
        ('FindWindowW', user32),
        ((1, 'lpClassName'), (1, 'lpWindowName'))
        )
FindWindow.errcheck = _errcheck_null

FindWindowEx = WINFUNCTYPE(HWND, HWND, HWND, LPCTSTR, LPCTSTR)(
        ('FindWindowExW', user32),
        ( (1, 'hwndParent'),
          (1, 'hwndChildAfter'),
          (1, 'lpClassName'),
          (1, 'lpWindowName'),
          )
        )
FindWindowEx.errcheck = _errcheck_null

PostMessage = WINFUNCTYPE(BOOL, HWND, UINT, WPARAM, LPARAM)(
        ('PostMessageW', user32),
        ((1, 'hWnd'), (1, 'Msg'), (1, 'wParam'), (1, 'lParam'))
        )
PostMessage.errcheck = _errcheck_false

SendMessage = WINFUNCTYPE(BOOL, HWND, UINT, WPARAM, LPARAM)(
        ('SendMessageW', user32),
        ((1, 'hWnd'), (1, 'Msg'), (1, 'wParam'), (1, 'lParam'))
        )

GetWindowLong = WINFUNCTYPE(LONG, HWND, c_int)(
        ('GetWindowLongW', user32),
        ((1, 'hWnd'), (1, 'nIndex'))
        )
GetWindowLong.errcheck = _errcheck_false

def get_aviutl_hwnd():
    hwnd = None
    try:
        hwnd = FindWindow(u'AviUtl', None)
    except WindowsError:
        raise AviUtlError(u'Not Aviutl window')
    try:
        while True:
            hwnd = GetWindowLong(hwnd, GWL_HWNDPARENT)
    except WindowsError:
        pass
    return hwnd

def get_aviutldl_hwnd(hwnd):
    hwndd = None
    hwndp = None
    for i in itertools.count():
        try:
            hwndd = FindWindowEx(None, None, u'#32770', None)
            hwndp = GetWindowLong(hwndd, GWL_HWNDPARENT)
            while hwndp != hwnd:
                hwndd = FindWindowEx(None, None, u'#32770', None)
                hwndp = GetWindowLong(hwndd, GWL_HWNDPARENT)
        except WindowsError:
            pass
        if hwndp == hwnd:
            break
        if i >= 20:
            raise AviUtlError(u'Not Aviutl window')
        time.sleep(0.5)
    time.sleep(2)
    return hwndd

def close():
    u"""ファイルを閉じる"""
    hwnd = get_aviutl_hwnd()
    PostMessage(hwnd, WM_COMMAND, 5157, 0)

def open(filename):
    u"""ファイルを開く"""
    c_filename = c_wchar_p(filename)

    hwnd = get_aviutl_hwnd()
    PostMessage(hwnd, WM_COMMAND, 5097, 0)

    hwnds = get_aviutldl_hwnd(hwnd)
    hwndb   = FindWindowEx(hwnds, None, u'Button', u'開く(&O)')
    hwndcbx = FindWindowEx(hwnds, None, u'ComboBoxEx32', None)
    hwndcb  = FindWindowEx(hwndcbx, None, u'ComboBox', None)
    hwnde   = FindWindowEx(hwndcb, None, u'Edit', None)
    SendMessage(hwnde, WM_SETTEXT, 0, cast(c_filename, c_void_p).value)
    SendMessage(hwndb, WM_LBUTTONDOWN, 0, 0)
    SendMessage(hwndb, WM_LBUTTONUP, 0, 0)

方針3 PyWin32

PyWin32 モジュールを使えば Windows API がつかえる。 ctypes よりも守備範囲が限定されているが、インポートするだけで Windows API をすぐに使えるのは大きなメリット。

方針4 Python/C API

CPython は C 言語でできているので C との連携は問題なくできる。大変そうと思いきや、ソースが結構流用できるのでそうでもない。思ったよりは早く出来上がるかも。

しかしデメリットが厳しい。とくに配布する時が。

メリット
  • 不明。 ctypes より早いのは確かだが、数時間待ちといった作業に対して数ミリ秒削ってなんになるというのか。
デメリット
  • (使う側の問題)ソースを配られても普通は困る。 ソースから setup.py するにはコンパイラを用意する必要がある。この時点ですでにむずかしいのに、さらに注意書きが加わる。 Visual Stadio を使うなら Python 本体をビルドしたものと同じものである必要がある。公式のインストーラーから入れた Python 2.6 なら VS2008 でよいが Python 2.5 だと VS2003 。これは入手困難だ。
  • (コードを書く側の問題) かといってビルド済みモジュールとして提供しようとすると、モジュールが Python のバージョンに依存しているのでバージョンごとに作るハメに。たとえば Python 2.5 用に作ったものは Python 2.4, 2,6 で動かない。古くても新しくてもダメ。
  • (コードを書く側使う側双方の問題)「AviUtl Control」 がバージョンアップした際、追随するには C コードの改変が必要。
_pyaviutl.c(未完成)

以下、途中まで作ったコード。 ctypes 版同様にファイルが開けて閉じれるところまで。 TODO, Windows API は ASCII 版になってしまっているが、 UNICODE 版を使うようにする。つまり UNICODE マクロを定義する。最近の Python は 9x 系では動かないし。

#include <Python.h>
#include <Windows.h>

/* モジュール例外 AviUtlError */
static PyObject *AviUtlError;

/* AviUtl の hwnd を取得する
 * 取得に失敗した場合、 AviUtlError 例外をセットし NULL を返す。
 */
static HWND get_aviutl_hwnd(void)
{
    HWND hwnd, hwndp;

    if((hwnd = FindWindow("AviUtl", NULL)) == NULL)
    {
        PyErr_SetString(AviUtlError, "Not AviUtl window.");
        return NULL;
    }
    while(hwndp = (HWND)GetWindowLong(hwnd, GWL_HWNDPARENT))
        hwnd = hwndp;
    return hwnd;
}

/* AviUtl の ダイアログの hwnd を取得する
 * 取得に失敗した場合、 AviUtlError 例外をセットし NULL を返す。
 */
static HWND getdlghwnd(HWND hwnd)
{
    HWND hwndd, hwndp;
    int i;

    Sleep(500);
    for(i = 0; ; i++){
        hwndd = FindWindowEx(NULL, NULL, "#32770", NULL);
        hwndp = (HWND)GetWindowLong(hwndd, GWL_HWNDPARENT);
        while(hwndd != 0 && hwndp != hwnd){
            hwndd = FindWindowEx(NULL, hwndd, "#32770", NULL);
            hwndp = (HWND)GetWindowLong(hwndd, GWL_HWNDPARENT);
        }
        if(hwndd != 0 && hwndp == hwnd) break;
        if(i >= 20){
            PyErr_SetString(
                    AviUtlError, "Dialog of output-plgin didnt appear.");
            return NULL;
        }
        Sleep(500);
    }
    Sleep(2000);

    return hwndd;
}

/* デバッグ用
 * AviUtl の hwnd を取得し、 Python の int型に強引に変換して返す
 */
static PyObject *
pyaviutl_get_aviutl_hwnd(PyObject *self, PyObject *args, PyObject *kwargs)
{
    HWND hwnd;

    static char *kwlist[] = {NULL};

    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "", kwlist))
        return NULL;

    hwnd = get_aviutl_hwnd();
    if(hwnd == NULL) return NULL;
    return Py_BuildValue("i", hwnd);
}

/* ファイルを閉じる */
static PyObject *
pyaviutl_close(PyObject *self, PyObject *args, PyObject *kwargs)
{
    HWND hwnd;

    static char *kwlist[] = {NULL};

    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "", kwlist))
        return NULL;

    hwnd = get_aviutl_hwnd();
    if(hwnd == NULL){
        /* hwnd 取得に失敗した。 
         * 例外 は get_aviutl_hwnd 関数がセットするので
         * ここでは NULL を返すだけでよい。
         */
        return NULL;
    }

    PostMessage(hwnd, WM_COMMAND, 5157, 0);

    Py_RETURN_NONE;
}

/* ファイルを開く */
static PyObject *
pyaviutl_open(PyObject *self, PyObject *args, PyObject *kwargs)
{
    HWND hwnd, hwnds, hwndb, hwndcbx, hwndcb, hwnde;
    char *filename = NULL;

    static char *kwlist[] = {"filename", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s", kwlist, &filename))
        return NULL;

    hwnd = get_aviutl_hwnd();
    if(hwnd == NULL){
        /* hwnd 取得に失敗した。 
         * 例外 は get_aviutl_hwnd 関数がセットするので
         * ここでは NULL を返すだけでよい。
         */
        return NULL;
    }

    PostMessage(hwnd, WM_COMMAND, 5097, 0);

    hwnds = getdlghwnd(hwnd);
    if(hwnds == NULL){
        /* hwnds 取得に失敗した。 
         * 例外 は getdlghwnd 関数がセットするので
         * ここでは NULL を返すだけでよい。
         */
        return NULL;
    }

    hwndb   = FindWindowEx(hwnds, NULL, "Button", "開く(&O)");
    hwndcbx = FindWindowEx(hwnds, NULL, "ComboBoxEx32", NULL);
    hwndcb  = FindWindowEx(hwndcbx, NULL, "ComboBox", NULL);
    hwnde   = FindWindowEx(hwndcb, NULL, "Edit", NULL);
    SendMessage(hwnde, WM_SETTEXT, 0, (LPARAM)filename);
    SendMessage(hwndb, WM_LBUTTONDOWN, 0, 0);
    SendMessage(hwndb, WM_LBUTTONUP, 0, 0);

    Py_RETURN_NONE;
}

static PyMethodDef module_methods[] = {
    {"get_aviutl_hwnd", (PyCFunction)pyaviutl_get_aviutl_hwnd,
     METH_VARARGS | METH_KEYWORDS,
     "get hwnd."},
    {"close", (PyCFunction)pyaviutl_close,
     METH_VARARGS | METH_KEYWORDS,
     "close File."},
    {"open", (PyCFunction)pyaviutl_open,
     METH_VARARGS | METH_KEYWORDS,
     "open File."},
    {NULL, NULL, 0, NULL} /* Sentinel */
};

PyMODINIT_FUNC
init_pyaviutl(void)
{
    PyObject* m;

    m = Py_InitModule3("_pyaviutl", module_methods,
                       "AviUtl.");
    if (m == NULL)
      return;

    AviUtlError = PyErr_NewException(
            "_pyaviutl.AviUtlError", PyExc_StandardError, NULL);
    Py_INCREF(AviUtlError);
    PyModule_AddObject(m, "AviUtlError", AviUtlError);

}
setup.py
from distutils.core import setup, Extension

pyaviutl = Extension('_pyaviutl',
                    sources=['_pyaviutl.c'],
                    libraries=['User32'])

setup (name = 'pyaviutl',
       version = '1.0',
       description = 'pyaviutl.',
       ext_modules = [pyaviutl])