From 8f430752f2b5a508dabe76fccfaf3f129a562e4b Mon Sep 17 00:00:00 2001 From: Ben Connors Date: Thu, 30 May 2019 20:29:03 -0400 Subject: Clean up audio submodule - Implement MPVPlayer - Restructure into submodule, split files --- blc/audio.py | 156 ------------------------------------------------- blc/audio/__init__.py | 23 ++++++++ blc/audio/ffplay.py | 84 ++++++++++++++++++++++++++ blc/audio/interface.py | 53 +++++++++++++++++ blc/audio/mpv.py | 48 +++++++++++++++ blc/audio/util.py | 9 +++ 6 files changed, 217 insertions(+), 156 deletions(-) delete mode 100755 blc/audio.py create mode 100644 blc/audio/__init__.py create mode 100644 blc/audio/ffplay.py create mode 100755 blc/audio/interface.py create mode 100644 blc/audio/mpv.py create mode 100644 blc/audio/util.py diff --git a/blc/audio.py b/blc/audio.py deleted file mode 100755 index 9df8853..0000000 --- a/blc/audio.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python3 - -"""Audio module for BLC. - -This module defines an AudioPlayer interface which allows for various audio backends to be used -interchangeably. It also defines a bare-bones better-than-nothing "FFPlayer" implementation and -a better "MPVPlayer" implementation. - -"DefaultAudioPlayer" should be used in general and will refer to MPVPlayer if available and -FFPlayer otherwise. -""" - -import atexit -import subprocess as subp -import time -import warnings - -from abc import ABC, abstractmethod, abstractproperty - -def ttoti(t): - """Convert seconds to milliseconds.""" - return int(1000*t + 0.5) - -def titot(ti): - """Convert milliseconds to seconds.""" - return ti/1000 - -class AudioPlayer(ABC): - """Class for playing audio. - - All time indices must be integers in milliseconds. - """ - @abstractmethod - def play(self, start=-1): - """Play the audio from the given time. - - If start is -1, play it from the current time index (e.g. if paused). If the player is - already playing, throw an error. - """ - return - - @abstractmethod - def seek(self, t): - """Seek to the given time index.""" - return - - @abstractmethod - def pause(self): - """Pause the player.""" - return - - @abstractmethod - def stop(self): - """Stop the player and reset to the first time index.""" - return - - @abstractproperty - def volume(self): - """Get or set the current volume.""" - return - - @abstractproperty - def position(self) -> int: - """The current position in milliseconds.""" - return - - @abstractproperty - def playing(self) -> bool: - """Return if the player is playing or not.""" - return - - def __init__(self, fname, args=()): - self.fname = fname - self.args = args - -class FFPlayer(AudioPlayer): - """Audio player using ffplay. - - Note that this is incredibly bad: the current position is guessed based on the start time of - the subprocess (meaning startup time of the ffplay process is counted in the current - position), no preloading of files is done, seeking is inaccurate and requires killing and - restarting the ffplay process, volume is ignored, and more. This is due to the fact that you - can't provide input to ffplay because it uses SDL exclusively for input (even though it can - be run without SDL?) so any change requires restarting the process. Use MPVPlayer if - possible. - """ - def play(self, start=-1): - if self.playing: - raise ValueError("Already playing") - - if start != -1: - self.start = titot(start) - if self.start <= 0.1: - self.start = 0 - self.player = subp.Popen(["ffplay", "-nodisp", "-autoexit", "-ss", str(self.start), *self.args, self.fname], - stdin=subp.DEVNULL, stdout=subp.DEVNULL, stderr=subp.DEVNULL) - atexit.register(self.stop) - self.start_time = time.monotonic() - - def stop(self): - if not self.playing: - return - self.player.terminate() - atexit.unregister(self.stop) - self.player = None - self.start = 0 - - def seek(self, t): - if self.playing: - self.stop() - self.start = titot(t) - self.play() - else: - self.start = titot(t) - - def pause(self): - if not self.playing: - return - self.stop() - self.start = self.start + time.monotonic() - - @property - def position(self): - if not self.playing: - return self.start - return ttoti(self.start + time.monotonic() - self.start_time) - - @property - def volume(self): - return 100 - - @volume.setter - def volume(self, vol): - return - - @property - def playing(self): - if self.player is not None: - if self.player.poll() is not None: - self.player = None - - return self.player is not None - - def __init__(self, fname, args=()): - super().__init__(fname, args=args) - - self.player = None - self.start = 0 - self.start_time = 0 - -## try: -## import mpv -## except (OSError, ImportError): -## warnings.warn("mpv backend unavailable, falling back to ffplay", RuntimeWarning) - -DefaultAudioPlayer = FFPlayer diff --git a/blc/audio/__init__.py b/blc/audio/__init__.py new file mode 100644 index 0000000..ebb9067 --- /dev/null +++ b/blc/audio/__init__.py @@ -0,0 +1,23 @@ +"""Audio module for BLC. + +This module defines an AudioPlayer interface which allows for various audio backends to be used +interchangeably. It also defines a bare-bones better-than-nothing "FFPlayer" implementation and +a better "MPVPlayer" implementation. + +"DefaultAudioPlayer" should be used in general and will refer to MPVPlayer if available and +FFPlayer otherwise. +""" + +from .interface import AudioPlayer +from .ffplay import FFPlayer + + +__all__ = ["AudioPlayer", "FFPlayer", "DefaultAudioPlayer"] + +try: + from .mpv import MPVPlayer + __all__.append("MPVPlayer") + DefaultAudioPlayer = MPVPlayer +except ImportError as e: + print(e) + DefaultAudioPlayer = FFPlayer diff --git a/blc/audio/ffplay.py b/blc/audio/ffplay.py new file mode 100644 index 0000000..306351a --- /dev/null +++ b/blc/audio/ffplay.py @@ -0,0 +1,84 @@ +"""Module for FFPlay-based audio player.""" + +import atexit +import subprocess as subp +import time + +from .interface import AudioPlayer +from .util import ttoti, titot + +class FFPlayer(AudioPlayer): + """Audio player using ffplay. + + Note that this is incredibly bad: the current position is guessed based on the start time of + the subprocess (meaning startup time of the ffplay process is counted in the current + position), no preloading of files is done, seeking is inaccurate and requires killing and + restarting the ffplay process, volume is ignored, and more. This is due to the fact that you + can't provide input to ffplay because it uses SDL exclusively for input (even though it can + be run without SDL?) so any change requires restarting the process. Use MPVPlayer if + possible. + """ + def play(self, start=-1): + if self.playing: + raise ValueError("Already playing") + + if start != -1: + self.start = titot(start) + if self.start <= 0.1: + self.start = 0 + self.player = subp.Popen(["ffplay", "-nodisp", "-autoexit", "-ss", str(self.start), *self.args, self.fname], + stdin=subp.DEVNULL, stdout=subp.DEVNULL, stderr=subp.DEVNULL) + atexit.register(self.stop) + self.start_time = time.monotonic() + + def stop(self): + if not self.playing: + return + self.player.terminate() + atexit.unregister(self.stop) + self.player = None + self.start = 0 + + def seek(self, t): + if self.playing: + self.stop() + self.start = titot(t) + self.play() + else: + self.start = titot(t) + + def pause(self): + if not self.playing: + return + self.stop() + self.start = self.start + time.monotonic() + + @property + def position(self): + if not self.playing: + return self.start + return ttoti(self.start + time.monotonic() - self.start_time) + + @property + def volume(self): + return 100 + + @volume.setter + def volume(self, vol): + return + + @property + def playing(self): + if self.player is not None: + if self.player.poll() is not None: + self.player = None + + return self.player is not None + + def __init__(self, fname, args=()): + super().__init__(fname, args=args) + + self.player = None + self.start = 0 + self.start_time = 0 + diff --git a/blc/audio/interface.py b/blc/audio/interface.py new file mode 100755 index 0000000..fff19cf --- /dev/null +++ b/blc/audio/interface.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +"""Module for the AudioPlayer interface.""" + +from abc import ABC, abstractmethod, abstractproperty + +class AudioPlayer(ABC): + """Class for playing audio. + + All time indices must be integers in milliseconds. + """ + @abstractmethod + def play(self, start=-1): + """Play the audio from the given time. + + If start is -1, play it from the current time index (e.g. if paused). If the player is + already playing, throw an error. + """ + return + + @abstractmethod + def seek(self, t): + """Seek to the given time index.""" + return + + @abstractmethod + def pause(self): + """Pause the player.""" + return + + @abstractmethod + def stop(self): + """Stop the player and reset to the first time index.""" + return + + @abstractproperty + def volume(self) -> int: + """Get or set the current volume.""" + return + + @abstractproperty + def position(self) -> int: + """The current position in milliseconds.""" + return + + @abstractproperty + def playing(self) -> bool: + """Return if the player is playing or not.""" + return + + def __init__(self, fname, args=()): + self.fname = fname + self.args = args diff --git a/blc/audio/mpv.py b/blc/audio/mpv.py new file mode 100644 index 0000000..da21494 --- /dev/null +++ b/blc/audio/mpv.py @@ -0,0 +1,48 @@ +"""Module for using MPV as the AudioPlayer backend.""" + +import mpv + +from .interface import AudioPlayer +from .util import titot, ttoti + +class MPVPlayer(AudioPlayer): + """Class for using MPV as an audio player.""" + @property + def position(self): + return ttoti(self.player.time_pos) + + def play(self, start=-1): + ## TODO: Raise error if already playing? + self.player.pause = False + + def pause(self): + self.player.pause = True + + def seek(self, t): + self.player.seek(titot(t), reference="absolute", precision="exact") + + def stop(self): + self.player.pause = True + self.seek(0) + + @property + def volume(self) -> int: + return int(self.player.volume) + + @volume.setter + def volume(self, value): + self.player.volume = value + + @property + def playing(self) -> bool: + if self.started: + return not (self.player.pause or self.player.eof_reached) + return False + + def __init__(self, fname, args=()): + super().__init__(fname, args) + self.player = mpv.MPV() + self.player.pause = True + self.player.keep_open = True + self.started = False + self.player.loadfile(fname) diff --git a/blc/audio/util.py b/blc/audio/util.py new file mode 100644 index 0000000..ed8345b --- /dev/null +++ b/blc/audio/util.py @@ -0,0 +1,9 @@ +"""Various AudioPlayer utilities.""" + +def ttoti(t): + """Convert seconds to milliseconds.""" + return int(1000*t + 0.5) + +def titot(ti): + """Convert milliseconds to seconds.""" + return ti/1000 -- cgit v1.2.3