summaryrefslogtreecommitdiff
path: root/audio.py
diff options
context:
space:
mode:
authorBen Connors <benconnors@outlook.com>2019-01-22 15:36:34 -0500
committerBen Connors <benconnors@outlook.com>2019-01-22 15:36:34 -0500
commitca498eba3d9eaa2c25e281f9f8e6b5c3c8646ba6 (patch)
tree3a39e50e4e605bb2f55a4f9a606a9f0b6003f7f6 /audio.py
Initial commit
- Finish and test workspace.Show rendering - Add some basic image visualization - Add some utility classes (audio, Tk)
Diffstat (limited to 'audio.py')
-rwxr-xr-xaudio.py154
1 files changed, 154 insertions, 0 deletions
diff --git a/audio.py b/audio.py
new file mode 100755
index 0000000..37b0f7d
--- /dev/null
+++ b/audio.py
@@ -0,0 +1,154 @@
+#!/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)
+ 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