AviUtl を Python から自動操縦する、のか?
動画のエンコードの際、世話になっているソフト、 AviUtl。これを Python から操ってみようと思ったのがことの始まり。
とはいえ、便利なものがすでに世に存在している。 「AviUtl Control」 。作者であるあじさんに感謝。
「AviUtl Control」は AviUtl をコマンドラインから操作する exe 郡。
方針1 subprocess モジュール
ファイルを開くには auc_open.exe, 閉じるには auc_close.exe といったように対応する exe を呼べばいいのだから subprocess モジュールでたたく。それだけ。味付けとして docstring をつけてみたり。
pyauc.py
# 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」は不要。
デメリット
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])