.mmlファイルを監視して上書き保存時にWine上のppmckで.mmlファイルから.nsfファイルへ自動で変換するためのGUIツールを作成(2009/12/17版)
(2010/2/17)「GNU/Linux上における ppmckについてのその後(2010/2/2現在)」にてGNU/Linux向けにビルドしたppmckが使えるようになっていることが分かり、変更監視/自動変換ツールもそれに合わせて「MMLファイルの変更を監視して上書き保存時にppmck(ppmckc/nesasm)でNSF変換を自動で行うツールを更新(2010/2/17版)」にて新しいバージョンを公開したため、この記事のスクリプトは古いものとなっている。
以下、以前の内容となる。
「Pythonでファイルの変更を監視する(OS非依存・PyGObject使用)」のファイル変更監視の仕組みを用いて、.mmlファイルの更新(上書き保存)時に「Wine上のppmckで.mmlファイルを.nsfファイルに変換する処理を自動化するPythonスクリプト(2009/12/7版)」で作成したスクリプトを実行し、更に、出力された.nsfファイルを自動的にプレーヤで再生するようなスクリプトを作成した。まだ作りの雑な部分が残っている気もするが、とりあえずこの状態で貼り付ける。
このスクリプトを「Wine上のppmckで.mmlファイルを.nsfファイルに変換する処理を自動化するPythonスクリプト(2009/12/7版)」のスクリプトと同じディレクトリに配置して実行し、対象の.mmlファイルを指定してボタンを押す(GUIファイルマネージャからのドラッグ・アンド・ドロップでも可)と監視が始まり、このファイルが更新されると自動で出力ファイルの変換と再生を行う。
再生に関しては、2009年12月現在、GNU/Linux向けの.nsf形式対応のオーディオプレーヤでppmckが対応する全ての(nesの)拡張音源が扱えるものはないため、(暫定で)Mednafenというソフトウェアの.nsfファイル再生機能を用いるようにしており、これが無ければ再生は行われない(別途インストールする必要がある)。
(2009/12/21)GNU/Linuxで動作するnsfプレーヤで拡張音源対応が十分なものをその後調べてみたところ、Festalon(http://projects.raphnet.net/#festalon)を見つけ、x86_64向けパッチを当ててconsole版をビルドした*1が、VRC7音源のデータでノイズが出る。Wine上のfoobar2000でfoo_input_nsfを用いると動作は良好。
[任意]ファイル名: mmlwatchergtk.py ライセンス: GPL-3 (or lator)
#! /usr/bin/python # -*- encoding: utf-8 -*- # mmlwatchergtk.py for ppmck 20091217 (C) 2009 kakurasan # compile MML file automatically when updated # Licensed under GPLv3+ import urllib import ctypes import time import sys import os try: import pygtk pygtk.require('2.0') except: pass try: import gtk except: print >> sys.stderr, 'Error: PyGTK is not installed' sys.exit(1) try: from glib import timeout_add_seconds as glib_timeout_add_seconds from glib import source_remove as glib_source_remove from glib import spawn_async as glib_spawn_async from glib import child_watch_add as glib_child_watch_add from glib import SPAWN_DO_NOT_REAP_CHILD as glib_SPAWN_DO_NOT_REAP_CHILD except: try: from gobject import timeout_add_seconds as glib_timeout_add_seconds from gobject import source_remove as glib_source_remove from gobject import spawn_async as glib_spawn_async from gobject import child_watch_add as glib_child_watch_add from gobject import SPAWN_DO_NOT_REAP_CHILD as glib_SPAWN_DO_NOT_REAP_CHILD except: print >> sys.stderr, 'Error: cannot import GLib functions' sys.exit(1) class MainWindow(gtk.Window): """ application main window """ def __init__(self, path, *args, **kwargs): gtk.Window.__init__(self, *args, **kwargs) # accelgroup self.__accelgroup = gtk.AccelGroup() self.add_accel_group(self.__accelgroup) # menuitems self.__item_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.__accelgroup) self.__menu_file = gtk.Menu() self.__menu_file.add(self.__item_quit) self.__item_file = gtk.MenuItem('_File') self.__item_file.set_submenu(self.__menu_file) self.__menubar = gtk.MenuBar() self.__menubar.append(self.__item_file) # entry for file path self.__entry = gtk.Entry() # browse self.__btn_browse = gtk.Button('...') # start/stop watching self.__btn_watch = gtk.ToggleButton('Watch') self.__btn_watch.set_sensitive(False) # message self.__textview = gtk.TextView() self.__textview.set_sensitive(False) self.__textbuf = self.__textview.get_buffer() self.__sw = gtk.ScrolledWindow() self.__sw.add(self.__textview) self.__sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) # containers self.__hbox = gtk.HBox() self.__hbox.pack_start(self.__entry) self.__hbox.pack_start(self.__btn_browse, expand=False, fill=False) self.__hbox.pack_start(self.__btn_watch, expand=False, fill=False) self.__vbox = gtk.VBox() self.__vbox.pack_start(self.__menubar, expand=False, fill=False) self.__vbox.pack_start(self.__hbox, expand=False, fill=False) self.__vbox.pack_start(self.__sw) # signals self.connect('delete_event', gtk.main_quit) self.__item_quit.connect('activate', gtk.main_quit) self.__entry.connect('changed', self.__on_entry_changed) self.__btn_browse.connect('clicked', self.__on_button_browse_clicked) self.__id_btnsig = self.__btn_watch.connect('toggled', self.__on_button_watch_toggled) self.connect('drag_data_received', self.__on_drag_data_received) self.__id_watch = None # window self.set_title('MMLWatcherGTK') self.add(self.__vbox) self.set_size_request(400, 300) # dnd self.__TARGET_TYPE_TEXT_URI_LIST = 12345 self.__dnd_list = [('text/uri-list', 0, self.__TARGET_TYPE_TEXT_URI_LIST),] self.__drag_set_acceptable(True) # path from argv if path: self.__entry.set_text(path) if os.access(path, os.R_OK): self.__btn_watch.set_active(True) # GLib self.glib = ctypes.cdll.LoadLibrary('libglib-2.0.so.0') def __on_entry_changed(self, widget): """ file path changed """ self.__btn_watch.set_sensitive(os.access(widget.get_text(), os.R_OK)) def __on_button_browse_clicked(self, widget): """ button clicked """ opendlg = gtk.FileChooserDialog(title='Select file', parent=self, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT)) opendlg.set_local_only(True) if opendlg.run() == gtk.RESPONSE_ACCEPT: self.__entry.set_text(opendlg.get_filename()) opendlg.destroy() def __on_button_watch_toggled(self, widget): """ toggled 'watch' button """ if widget.get_active(): # start watching try: self.__mtime = os.stat(self.__entry.get_text()).st_mtime except OSError, (errno, msg): self.__append_log_and_scroll('cannot stat "%s"\n errno: %d\n message: %s\n' % (self.__entry.get_text(), errno, msg)) # block handler and set 'inactive' self.__btn_watch.handler_block(self.__id_btnsig) self.__btn_watch.set_active(False) self.__btn_watch.handler_unblock(self.__id_btnsig) return self.__entry.set_sensitive(False) self.__btn_browse.set_sensitive(False) self.__id_watch = glib_timeout_add_seconds(1, self.__to_watch_file) self.__append_log_and_scroll('start watching "%s" (%s)\n' % (self.__entry.get_text(), time.ctime(None))) else: # stop watching glib_source_remove(self.__id_watch) self.__id_watch = None self.__append_log_and_scroll('stop watching "%s"\n' % self.__entry.get_text()) self.__entry.set_sensitive(True) self.__btn_browse.set_sensitive(True) def __append_log_and_scroll(self, text): """ append log to TextBuffer and scroll TextView """ self.__textbuf.place_cursor(self.__textbuf.get_end_iter()) self.__textbuf.insert_at_cursor(text) self.__textview.scroll_to_mark(self.__textbuf.get_insert(), 0) def __to_watch_file(self): """ watch timestamp / autocompile / launch player """ try: new_mtime = os.stat(self.__entry.get_text()).st_mtime except OSError, (errno, msg): self.__append_log_and_scroll('cannot stat "%s"\n errno: %d\n message: %s\n' % (self.__entry.get_text(), errno, msg)) self.__btn_watch.set_active(False) return False # check timestamp if self.__mtime != new_mtime: self.__mtime = new_mtime # store new timestamp # file is updated self.__append_log_and_scroll('file "%s" updated (%s)\n' % (self.__entry.get_text(), time.ctime(self.__mtime))) self.__drag_set_acceptable(False) # autocompile script pid_autocompile = glib_spawn_async([os.path.join(os.path.dirname(__file__), 'mml2nsf.py'), self.__entry.get_text()], flags=glib_SPAWN_DO_NOT_REAP_CHILD)[0] self.__id_childwatch = glib_child_watch_add(pid_autocompile, self.__cb_close_child) return True; def __cb_close_child(self, pid, status): """ child(autocompile script) exited """ self.__append_log_and_scroll('compiled "%s"\n' % self.__entry.get_text()) self.__drag_set_acceptable(True) # remove event source for childwatch glib_source_remove(self.__id_childwatch) # close pid self.glib.g_spawn_close_pid(pid) # launch nsf player glib_spawn_async(['/usr/bin/mednafen', os.path.join(os.path.abspath(os.path.dirname(__file__)), 'out', '%s.nsf' % os.path.splitext(os.path.basename(self.__entry.get_text()))[0])]) def __drag_set_acceptable(self, acceptable): """ set acceptable status """ if acceptable == True: self.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP, self.__dnd_list, gtk.gdk.ACTION_COPY) else: self.drag_dest_unset() def __on_drag_data_received(self, widget, context, x, y, selection, info, time): """ received drag data """ item = selection.data.rstrip('\x00').splitlines()[-1] # use only last one if item.startswith('file:'): path = os.path.normpath(urllib.url2pathname(item[5:])) # 5=len('file:') if self.__id_watch: # now watching self.__btn_watch.set_active(False) # stop watching self.__entry.set_text(path) # update entry self.__btn_watch.set_active(True) # start watching class MMLWatcherGTK: """ MML watcher for ppmck """ def main(self): """ main """ path = None if len(sys.argv) > 1: path = sys.argv[1] win = MainWindow(path) win.show_all() gtk.main() if __name__ == '__main__': app = MMLWatcherGTK() app.main()
Mednafenに関するメモ
巻き戻し
.nsfファイルはF10で最初から再生し直せる。出力サウンドシステムと出力デバイスの指定
ALSAサウンドシステムの既定のPCMデバイス(プラグイン)に出力する場合、下のような設定を行う。[一部]ファイル名: ~/.mednafen/mednafen.cfg
;Select sound driver. sounddriver alsa ;Select sound output device. sounddevice sexyal-literal-default
「sounddevice」にはALSAのPCMデバイス(プラグイン)名を指定でき、別途「type pulse」なPCMを定義(関連記事)し、これを指定することで、PulseAudioへ出力することもできる。
JACK Audio Connection Kitへ出力するには下のように設定する。
[一部]ファイル名: ~/.mednafen/mednafen.cfg
;Select sound driver. sounddriver jack ;Select sound output device. sounddevice default
使用したバージョン:
- Wine 1.1.34
- ppmck 09
- Mednafen 0.8.C (0.8.12)
*1:./configure --enable-interface=console; make