Spaces:
Running
Running
| # This file is part of audioread. | |
| # Copyright 2014, Adrian Sampson. | |
| # | |
| # Permission is hereby granted, free of charge, to any person obtaining | |
| # a copy of this software and associated documentation files (the | |
| # "Software"), to deal in the Software without restriction, including | |
| # without limitation the rights to use, copy, modify, merge, publish, | |
| # distribute, sublicense, and/or sell copies of the Software, and to | |
| # permit persons to whom the Software is furnished to do so, subject to | |
| # the following conditions: | |
| # | |
| # The above copyright notice and this permission notice shall be | |
| # included in all copies or substantial portions of the Software. | |
| """Read audio data using the ffmpeg command line tool via its standard | |
| output. | |
| """ | |
| import queue | |
| import re | |
| import subprocess | |
| import sys | |
| import threading | |
| import time | |
| from io import DEFAULT_BUFFER_SIZE | |
| from .exceptions import DecodeError | |
| from .base import AudioFile | |
| COMMANDS = ('ffmpeg', 'avconv') | |
| if sys.platform == "win32": | |
| PROC_FLAGS = 0x08000000 | |
| else: | |
| PROC_FLAGS = 0 | |
| class FFmpegError(DecodeError): | |
| pass | |
| class CommunicationError(FFmpegError): | |
| """Raised when the output of FFmpeg is not parseable.""" | |
| class UnsupportedError(FFmpegError): | |
| """The file could not be decoded by FFmpeg.""" | |
| class NotInstalledError(FFmpegError): | |
| """Could not find the ffmpeg binary.""" | |
| class ReadTimeoutError(FFmpegError): | |
| """Reading from the ffmpeg command-line tool timed out.""" | |
| class QueueReaderThread(threading.Thread): | |
| """A thread that consumes data from a filehandle and sends the data | |
| over a Queue. | |
| """ | |
| def __init__(self, fh, blocksize=1024, discard=False): | |
| super().__init__() | |
| self.fh = fh | |
| self.blocksize = blocksize | |
| self.daemon = True | |
| self.discard = discard | |
| self.queue = None if discard else queue.Queue() | |
| def run(self): | |
| while True: | |
| data = self.fh.read(self.blocksize) | |
| if not self.discard: | |
| self.queue.put(data) | |
| if not data: | |
| # Stream closed (EOF). | |
| break | |
| def popen_multiple(commands, command_args, *args, **kwargs): | |
| """Like `subprocess.Popen`, but can try multiple commands in case | |
| some are not available. | |
| `commands` is an iterable of command names and `command_args` are | |
| the rest of the arguments that, when appended to the command name, | |
| make up the full first argument to `subprocess.Popen`. The | |
| other positional and keyword arguments are passed through. | |
| """ | |
| for i, command in enumerate(commands): | |
| cmd = [command] + command_args | |
| try: | |
| return subprocess.Popen(cmd, *args, **kwargs) | |
| except OSError: | |
| if i == len(commands) - 1: | |
| # No more commands to try. | |
| raise | |
| def available(): | |
| """Detect whether the FFmpeg backend can be used on this system. | |
| """ | |
| try: | |
| proc = popen_multiple( | |
| COMMANDS, | |
| ['-version'], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| creationflags=PROC_FLAGS, | |
| ) | |
| except OSError: | |
| return False | |
| else: | |
| proc.wait() | |
| return proc.returncode == 0 | |
| # For Windows error switch management, we need a lock to keep the mode | |
| # adjustment atomic. | |
| windows_error_mode_lock = threading.Lock() | |
| class FFmpegAudioFile(AudioFile): | |
| """An audio file decoded by the ffmpeg command-line utility.""" | |
| def __init__(self, filename, block_size=DEFAULT_BUFFER_SIZE): | |
| # On Windows, we need to disable the subprocess's crash dialog | |
| # in case it dies. Passing SEM_NOGPFAULTERRORBOX to SetErrorMode | |
| # disables this behavior. | |
| windows = sys.platform.startswith("win") | |
| if windows: | |
| windows_error_mode_lock.acquire() | |
| SEM_NOGPFAULTERRORBOX = 0x0002 | |
| import ctypes | |
| # We call SetErrorMode in two steps to avoid overriding | |
| # existing error mode. | |
| previous_error_mode = \ | |
| ctypes.windll.kernel32.SetErrorMode(SEM_NOGPFAULTERRORBOX) | |
| ctypes.windll.kernel32.SetErrorMode( | |
| previous_error_mode | SEM_NOGPFAULTERRORBOX | |
| ) | |
| try: | |
| self.proc = popen_multiple( | |
| COMMANDS, | |
| ['-i', filename, '-f', 's16le', '-'], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| stdin=subprocess.DEVNULL, | |
| creationflags=PROC_FLAGS, | |
| ) | |
| except OSError: | |
| raise NotInstalledError() | |
| finally: | |
| # Reset previous error mode on Windows. (We can change this | |
| # back now because the flag was inherited by the subprocess; | |
| # we don't need to keep it set in the parent process.) | |
| if windows: | |
| try: | |
| import ctypes | |
| ctypes.windll.kernel32.SetErrorMode(previous_error_mode) | |
| finally: | |
| windows_error_mode_lock.release() | |
| # Start another thread to consume the standard output of the | |
| # process, which contains raw audio data. | |
| self.stdout_reader = QueueReaderThread(self.proc.stdout, block_size) | |
| self.stdout_reader.start() | |
| # Read relevant information from stderr. | |
| self._get_info() | |
| # Start a separate thread to read the rest of the data from | |
| # stderr. This (a) avoids filling up the OS buffer and (b) | |
| # collects the error output for diagnosis. | |
| self.stderr_reader = QueueReaderThread(self.proc.stderr) | |
| self.stderr_reader.start() | |
| def read_data(self, timeout=10.0): | |
| """Read blocks of raw PCM data from the file.""" | |
| # Read from stdout in a separate thread and consume data from | |
| # the queue. | |
| start_time = time.time() | |
| while True: | |
| # Wait for data to be available or a timeout. | |
| data = None | |
| try: | |
| data = self.stdout_reader.queue.get(timeout=timeout) | |
| if data: | |
| yield data | |
| else: | |
| # End of file. | |
| break | |
| except queue.Empty: | |
| # Queue read timed out. | |
| end_time = time.time() | |
| if not data: | |
| if end_time - start_time >= timeout: | |
| # Nothing interesting has happened for a while -- | |
| # FFmpeg is probably hanging. | |
| raise ReadTimeoutError('ffmpeg output: {}'.format( | |
| b''.join(self.stderr_reader.queue.queue) | |
| )) | |
| else: | |
| start_time = end_time | |
| # Keep waiting. | |
| continue | |
| def _get_info(self): | |
| """Reads the tool's output from its stderr stream, extracts the | |
| relevant information, and parses it. | |
| """ | |
| out_parts = [] | |
| while True: | |
| line = self.proc.stderr.readline() | |
| if not line: | |
| # EOF and data not found. | |
| raise CommunicationError("stream info not found") | |
| # In Python 3, result of reading from stderr is bytes. | |
| if isinstance(line, bytes): | |
| line = line.decode('utf8', 'ignore') | |
| line = line.strip().lower() | |
| if 'no such file' in line: | |
| raise OSError('file not found') | |
| elif 'invalid data found' in line: | |
| raise UnsupportedError() | |
| elif 'duration:' in line: | |
| out_parts.append(line) | |
| elif 'audio:' in line: | |
| out_parts.append(line) | |
| self._parse_info(''.join(out_parts)) | |
| break | |
| def _parse_info(self, s): | |
| """Given relevant data from the ffmpeg output, set audio | |
| parameter fields on this object. | |
| """ | |
| # Sample rate. | |
| match = re.search(r'(\d+) hz', s) | |
| if match: | |
| self.samplerate = int(match.group(1)) | |
| else: | |
| self.samplerate = 0 | |
| # Channel count. | |
| match = re.search(r'hz, ([^,]+),', s) | |
| if match: | |
| mode = match.group(1) | |
| if mode == 'stereo': | |
| self.channels = 2 | |
| else: | |
| cmatch = re.match(r'(\d+)\.?(\d)?', mode) | |
| if cmatch: | |
| self.channels = sum(map(int, cmatch.group().split('.'))) | |
| else: | |
| self.channels = 1 | |
| else: | |
| self.channels = 0 | |
| # Duration. | |
| match = re.search( | |
| r'duration: (\d+):(\d+):(\d+).(\d)', s | |
| ) | |
| if match: | |
| durparts = list(map(int, match.groups())) | |
| duration = ( | |
| durparts[0] * 60 * 60 + | |
| durparts[1] * 60 + | |
| durparts[2] + | |
| float(durparts[3]) / 10 | |
| ) | |
| self.duration = duration | |
| else: | |
| # No duration found. | |
| self.duration = 0 | |
| def close(self): | |
| """Close the ffmpeg process used to perform the decoding.""" | |
| if hasattr(self, 'proc'): | |
| # First check the process's execution status before attempting to | |
| # kill it. This fixes an issue on Windows Subsystem for Linux where | |
| # ffmpeg closes normally on its own, but never updates | |
| # `returncode`. | |
| self.proc.poll() | |
| # Kill the process if it is still running. | |
| if self.proc.returncode is None: | |
| self.proc.kill() | |
| self.proc.wait() | |
| # Wait for the stream-reading threads to exit. (They need to | |
| # stop reading before we can close the streams.) | |
| if hasattr(self, 'stderr_reader'): | |
| self.stderr_reader.join() | |
| if hasattr(self, 'stdout_reader'): | |
| self.stdout_reader.join() | |
| # Close the stdout and stderr streams that were opened by Popen, | |
| # which should occur regardless of if the process terminated | |
| # cleanly. | |
| self.proc.stdout.close() | |
| self.proc.stderr.close() | |
| def __del__(self): | |
| self.close() | |
| # Iteration. | |
| def __iter__(self): | |
| return self.read_data() | |
| # Context manager. | |
| def __enter__(self): | |
| return self | |
| def __exit__(self, exc_type, exc_val, exc_tb): | |
| self.close() | |
| return False | |