試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

.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()

  1. Mednafenに関するメモ
    1. 巻き戻し
    2. 出力サウンドシステムと出力デバイスの指定

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