summaryrefslogtreecommitdiff
path: root/audio.py
blob: 37b0f7df762db4777a15bb4984629a902d82adf5 (plain)
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