About | Log | Files | Refs
commit 8f430752f2b5a508dabe76fccfaf3f129a562e4b
parent 9c464c705ef8ef0ecc2aa674beaaf89460e70d3d
Author: Ben Connors <benconnors@outlook.com>
Date: Thu, 30 May 2019 20:29:03 -0400
Clean up audio submodule
- Implement MPVPlayer
- Restructure into submodule, split files
Diffstat:
6 files changed, 217 insertions(+), 156 deletions(-)
diff --git a/blc/audio.py b/blc/audio.py
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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