1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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
|