import logging import os import signal import subprocess import sys import time class AudioSource: def __init__(self, device: str, audio_system: str = 'alsa', sample_rate: int = 44100, bitrate: int = 128, channels: int = 1, ffmpeg_bin: str = 'ffmpeg', bufsize: int = 8192, debug: bool = False): self.ffmpeg_bin = ffmpeg_bin self.debug = debug self.bufsize = bufsize self.devnull = None self.ffmpeg = None self.logger = logging.getLogger(self.__class__.__name__) self.logger.setLevel(logging.DEBUG if self.debug else logging.INFO) self.ffmpeg_args = ( ffmpeg_bin, '-f', audio_system, '-i', device, '-vn', '-acodec', 'libmp3lame', '-b:a', str(bitrate) + 'k', '-ac', str(channels), '-ar', str(sample_rate), '-f', 'mp3', '-') self._ffmpeg_start_time = None self._first_sample_time = None self.latency = 0. def __iter__(self): return self def __next__(self) -> bytes: if not self.ffmpeg or self.ffmpeg.poll() is not None: raise StopIteration while True: data = self.ffmpeg.stdout.read(self.bufsize) if not data: break if not self._first_sample_time: self._first_sample_time = time.time() self.latency = self._first_sample_time - self._ffmpeg_start_time self.logger.info('Estimated latency: {} msec'.format(int(self.latency * 1000))) if time.time() - self._first_sample_time >= self.latency: return data raise StopIteration def __enter__(self): kwargs = dict(stdout=subprocess.PIPE) if not self.debug: self.devnull = open(os.devnull, 'w') kwargs['stderr'] = self.devnull self.logger.info('Running FFmpeg: {}'.format(' '.join(self.ffmpeg_args))) self.ffmpeg = subprocess.Popen(self.ffmpeg_args, **kwargs) self._ffmpeg_start_time = time.time() return self def __exit__(self, exc_type, exc_val, exc_tb): if self.ffmpeg: self.ffmpeg.terminate() try: self.ffmpeg.wait(timeout=5) except subprocess.TimeoutExpired: self.logger.warning('FFmpeg process termination timeout') if self.ffmpeg.poll() is None: self.ffmpeg.kill() self.ffmpeg.wait() self.ffmpeg = None if self.devnull: self.devnull.close() self.devnull = None self._ffmpeg_start_time = None self._first_sample_time = None def pause(self): if not self.ffmpeg: return self.ffmpeg.send_signal(signal.SIGSTOP) def resume(self): if not self.ffmpeg: return self.ffmpeg.send_signal(signal.SIGCONT) # vim:sw=4:ts=4:et: