# control.py --- Python interface to libbeep -- control module. # Copyright (c) 2005 Scott Grayban # # This file is part of PyBMP. # # PyBMP is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 dated June, 1991. # # PyBMP is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; see the file COPYING. If not, write to the # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, # Boston, MA 02110-1301 USA. # # $Id: control.py 3 2006-03-28 11:52:53Z sgrayban $ # """Python interface to BMP --- control module. This module provides a Python interface to control BMP (the X MultiMedia System), an audio and video player for Unix-like platforms. This module provides bindings for all the bmp_remote_* functions accessible through the libbmp library (which comes with BMP), plus a few higher-level functions that I (Florent Rougon) find useful. The function names and mappings between the calling syntax of the C functions from libbmp and that of their Python bindings are meant to be mechanical (see below). Note: I use the expression "Python binding for foo()" for a Python function that wraps (thus calls) directly, not doing additional work, the C function foo() defined in libbmp. Function names -------------- The binding for the libbmp function bmp_remote_foo() will be called foo; therefore, you will probably use it in this way: import bmp.control res = bmp.control.foo(arg, ...) Calling syntax -- passing arguments and getting results ------------------------------------------------------- Each bmp_remote_* function from libbmp takes as its first argument the BMP session to control. With bmp.control, this argument is optional (defaulting to 0, which is generally what you want if you don't launch multiple BMP sessions at once) and comes last. For the other arguments: - the type mapping should be obvious (if the C function expects a gint, the Python binding expects a Python integer, same for char * and strings, gfloats and floats, etc.); when the C function expects a list (such as a GList *), the Python binding expects a sequence (like a list or a tuple). gboolean types are mapped to Python integers (FALSE is mapped to 0 and TRUE to 1). Except for 'session', which has to come last so as to be optional, the order of the arguments is always preserved. Example: void bmp_remote_set_volume(gint session, gint vl, gint vr) is mapped to: set_volume(vl, vr, session) where 'vl' and 'vr' have to be Python integers and 'session' (also an integer), is optional and defaults to 0. Note: 'vl' and 'vr' stand for left and right volume, respectively. - if the C function returns values through the use of pointers, these are returned by the corresponding Python binding and therefore are removed from the argument list of the Python binding. The type/structure of the return value is the most obvious one I could think of (e.g. a single integer if the function returns a gboolean). The most non-obvious example is indeed quite simple, as you can see: void bmp_remote_get_eq(gint session, gfloat *preamp, gfloat **bands) which returns a global preamp gain (in dB) and a list of gains for 10 frequeny bands. It is called like this in Python: (preamp, bands) = get_eq(session) where 'session' is optional (see above), 'preamp' is a float and 'bands' is a 10-tuple of floats. Note: this is written from this module's scope, but in real life, you would of course probably have: import bmp.control [...] (preamp, bands) = bmp.control.get_eq(session) Functions exported by this module --------------------------------- * From libbmp playlist playlist_add playlist_delete playlist_clear playlist_add_url_string playlist_ins_url_string get_playlist_length get_playlist_pos set_playlist_pos get_playlist_file get_playlist_title get_playlist_time play pause play_pause stop eject playlist_prev playlist_next jump_to_time is_running is_playing is_paused get_output_time get_info get_volume set_volume get_main_volume set_main_volume get_balance set_balance get_eq set_eq get_eq_preamp set_eq_preamp get_eq_band set_eq_band get_skin set_skin main_win_toggle pl_win_toggle eq_win_toggle is_main_win is_pl_win is_eq_win show_prefs_box show_about_box toggle_aot toggle_repeat toggle_shuffle is_repeat is_shuffle get_version quit play_files (deprecated in libbmp) * Specific to this module (no direct binding in libbmp) playlist_add_allow_relative enqueue_and_play enqueue_and_play_launch_if_session_not_started fade_out Exceptions specific to this module ---------------------------------- ExecutableNotFound RequestedSessionDoesNotComeUp InvalidFadeOutAction They are all subclasses of bmp.error. """ import re, os, os.path, string, time import common from _bmpcontrol import * class ExecutableNotFound(common.error): """Exception raised when the BMP executable can't be found.""" ExceptionShortDescription = "Executable not found" class RequestedSessionDoesNotComeUp(common.error): """Exception raised when a started BMP session still doesn't answer after a specified timeout.""" ExceptionShortDescription = "Requested session doesn't come up" class InvalidFadeOutAction(common.error): """Exception raised when fade_out is given an invalid action.""" ExceptionShortDescription = "Invalid action" def _find_in_path(prog_name): """Search an executable in the PATH, like the exec*p functions do. If PATH is not defined, the default path ":/bin:/usr/bin" is used, as with the C library exec*p functions. Return the absolute file name or None if no readable and executable file is found. """ PATH = os.getenv("PATH", ":/bin:/usr/bin") # see the execvp(3) man page for dir in string.split(PATH, ":"): full_path = os.path.join(dir, prog_name) if os.path.isfile(full_path) \ and os.access(full_path, os.R_OK | os.X_OK): return full_path return None def _find_and_check_executable(prog_name): """Return the absolute file name if OK, None otherwise.""" if prog_name[0:2] == "./" or prog_name[0:3] == "../": abs_file_name = os.path.join(os.getcwd(), prog_name) elif os.path.isabs(prog_name): abs_file_name = prog_name else: # This checks the r and x bits return _find_in_path(prog_name) if os.path.isfile(abs_file_name) and \ os.access(abs_file_name, os.R_OK | os.X_OK): return abs_file_name else: return None # Same as playlist_add but converts all relative paths to absolute def playlist_add_allow_relative(seq, session=0): """Add files/URLs to the playlist, allowing relative file names. seq -- a sequence of files/URLs session -- the BMP session to act on Return None. """ # Regexp matching absolute paths and strings containing "://" abs_re = re.compile(r"/|.*://") abs_seq = list(seq) for i in range(len(seq)): if not (abs_re.match(seq[i])): abs_seq[i] = os.path.join(os.getcwd(), seq[i]) playlist_add(abs_seq, session) def enqueue_and_play(seq, session=0): """Add files/URLs to the playlist and start playing from the first one. seq -- a sequence of files/URLs session -- the BMP session to act on The files/URLs in seq are added to the playlist and BMP is asked to play starting at the first element of seq. Return None. """ pl = get_playlist_length(session) playlist_add_allow_relative(seq, session) set_playlist_pos(pl, session) play() def enqueue_and_play_launch_if_session_not_started(seq, bmp_prg="beep-media-player", session=0, poll_delay=0.1, timeout=10.0): """Add files/URLs to the playlist and start playing from the first one. seq -- a sequence of files/URLs bmp_prg -- the name (absolute or looked up in the PATH) of an BMP binary to invoke in case the specified session is not running session -- the BMP session to act on poll_delay -- poll delay, in seconds (float, see below) timeout -- timeout while polling, in seconds (float, see below) This function is identical to enqueue_and_play except that it spawns an BMP process if the requested session is not running. When it does spawn an BMP process, it has to wait until BMP is ready to handle requests (here, the first request will be enqueue_and_play). It will therefore check every 'poll_delay' seconds whether the requested session is ready, and abort after 'timeout' seconds of unsuccessful checks, raising bmp.control.RequestedSessionDoesNotComeUp. If we get that far, it may be that your system is very slow, or more likely that the BMP session that was started by this function is not the one numbered 'session': there is currently no way to start an BMP session for a chosen number; we have to guess the number of the session that will be started... Return None. Notable exceptions: - bmp.control.ExecutableNotFound is raised if 'bmp_prg' can't be found, read and executed. - bmp.control.RequestedSessionDoesNotComeUp is raised if the requested session is still unable to handle requests 'timeout' seconds after the BMP process was started by this function. """ if not is_running(session): # We want to warn the user if the BMP executable can't be # found or executed, so we have to check *before* forking. abs_prog_name = _find_and_check_executable(bmp_prg) if not abs_prog_name: raise ExecutableNotFound("can't find BMP executable") child_pid = os.fork() if child_pid == 0: # We are in the child, me MUST NOT trigger any exception (look at # _spawnvef in Python's os.py). try: os.execvp(abs_prog_name, (abs_prog_name,)) except: # We cannot know in the father process if the child's execvp # failed, but AFAIK, there is no simple solution since I don't # want the father to wait for the child's completion. init # will be a good father. :-) os._exit(127) else: # We are in the father start_time = time.time() while not is_running(session): if time.time() - start_time >= timeout: raise RequestedSessionDoesNotComeUp( "session %u still unavailable after %.2f seconds " "timeout" % (session, timeout)) time.sleep(poll_delay) enqueue_and_play(seq, session) def fade_out(action="stop", nb_steps=20, step_duration=0.5, restore_volume=1, session=0): """Fade out the volume to stop or pause the playback. Progressively decrease the main volume, then stop or pause (depending on the 'action' argument), then optionally restore the original main volume setting. action -- a string, either "stop" or "pause" nb_steps -- number of decrease-volume steps to use step_duration -- duration of a step (float, in seconds) restore_volume -- boolean (0 = false, 1 = true) telling whether to restore the original main volume setting after the fade out session -- the BMP session to act on Return None. Notable exception: bmp.control.InvalidFadeOutAction is raised if 'action' is invalid. """ vol = orig_volume = get_main_volume(session) # int() to be safe with Python >= 3.0 while still working before 2.2 # (first Python version with the // floor division) # Also, int() returns an integer, contrary to math.floor(). decr = int(orig_volume / nb_steps) for i in range(nb_steps): vol = vol - decr set_main_volume(vol, session) time.sleep(step_duration) # Stop/pause the playback and restore the volume as it was before the fade # out. if action == "stop": stop(session) elif action == "pause": pause(session) else: raise InvalidFadeOutAction("invalid action for fade_out: %s" % action) if restore_volume: set_main_volume(orig_volume)