#!/usr/bin/env python
# coding=utf-8
# aeneas is a Python/C library and a set of tools
# to automagically synchronize audio and text (aka forced alignment)
#
# Copyright (C) 2012-2013, Alberto Pettarin (www.albertopettarin.it)
# Copyright (C) 2013-2015, ReadBeyond Srl (www.readbeyond.it)
# Copyright (C) 2015-2017, Alberto Pettarin (www.albertopettarin.it)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
This module contains the following classes:
* :class:`~aeneas.ffmpegwrapper.FFMPEGWrapper`, a wrapper around ``ffmpeg`` to convert audio files;
* :class:`~aeneas.ffmpegwrapper.FFMPEGPathError`, representing a failure to locate the ``ffmpeg`` executable.
"""
from __future__ import absolute_import
from __future__ import print_function
import subprocess
from aeneas.logger import Loggable
from aeneas.runtimeconfiguration import RuntimeConfiguration
import aeneas.globalfunctions as gf
[docs]class FFMPEGPathError(Exception):
"""
Error raised when the path to ``ffmpeg`` is not a valid executable.
.. versionadded:: 1.4.1
"""
pass
[docs]class FFMPEGWrapper(Loggable):
"""
A wrapper around ``ffmpeg`` to convert audio files.
In abstract terms, it will perform a call like::
$ ffmpeg -i /path/to/input.mp3 [parameters] /path/to/output.wav
:param rconf: a runtime configuration
:type rconf: :class:`~aeneas.runtimeconfiguration.RuntimeConfiguration`
:param logger: the logger object
:type logger: :class:`~aeneas.logger.Logger`
"""
FFMPEG_SAMPLE_8000 = ["-ar", "8000"]
""" Single parameter for ``ffmpeg``: 8000 Hz sampling """
FFMPEG_SAMPLE_16000 = ["-ar", "16000"]
""" Single parameter for ``ffmpeg``: 16000 Hz sampling """
FFMPEG_SAMPLE_22050 = ["-ar", "22050"]
""" Single parameter for ``ffmpeg``: 22050 Hz sampling """
FFMPEG_SAMPLE_44100 = ["-ar", "44100"]
""" Single parameter for ``ffmpeg``: 44100 Hz sampling """
FFMPEG_SAMPLE_48000 = ["-ar", "48000"]
""" Single parameter for ``ffmpeg``: 48000 Hz sampling """
FFMPEG_MONO = ["-ac", "1"]
""" Single parameter for ``ffmpeg``: mono (1 channel) """
FFMPEG_STEREO = ["-ac", "2"]
""" Single parameter for ``ffmpeg``: stereo (2 channels) """
FFMPEG_OVERWRITE = ["-y"]
""" Single parameter for ``ffmpeg``: force overwriting output file """
FFMPEG_PLAIN_HEADER = ["-map_metadata", "-1", "-flags", "+bitexact"]
""" Single parameter for ``ffmpeg``: generate WAVE header
without extra chunks (e.g., the INFO chunk) """
FFMPEG_FORMAT_WAVE = ["-f", "wav"]
""" Single parameter for ``ffmpeg``: produce output in ``wav`` format
(must be the second to last argument to ``ffmpeg``,
just before path of the output file) """
FFMPEG_PARAMETERS_SAMPLE_KEEP = (
FFMPEG_MONO +
FFMPEG_OVERWRITE +
FFMPEG_PLAIN_HEADER +
FFMPEG_FORMAT_WAVE
)
""" Set of parameters for ``ffmpeg`` without changing the sampling rate """
FFMPEG_PARAMETERS_SAMPLE_8000 = (
FFMPEG_MONO +
FFMPEG_SAMPLE_8000 +
FFMPEG_OVERWRITE +
FFMPEG_PLAIN_HEADER +
FFMPEG_FORMAT_WAVE
)
""" Set of parameters for ``ffmpeg`` with 8000 Hz sampling """
FFMPEG_PARAMETERS_SAMPLE_16000 = (
FFMPEG_MONO +
FFMPEG_SAMPLE_16000 +
FFMPEG_OVERWRITE +
FFMPEG_PLAIN_HEADER +
FFMPEG_FORMAT_WAVE
)
""" Set of parameters for ``ffmpeg`` with 16000 Hz sampling """
FFMPEG_PARAMETERS_SAMPLE_22050 = (
FFMPEG_MONO +
FFMPEG_SAMPLE_22050 +
FFMPEG_OVERWRITE +
FFMPEG_PLAIN_HEADER +
FFMPEG_FORMAT_WAVE
)
""" Set of parameters for ``ffmpeg`` with 22050 Hz sampling """
FFMPEG_PARAMETERS_SAMPLE_44100 = (
FFMPEG_MONO +
FFMPEG_SAMPLE_44100 +
FFMPEG_OVERWRITE +
FFMPEG_PLAIN_HEADER +
FFMPEG_FORMAT_WAVE
)
""" Set of parameters for ``ffmpeg`` with 44100 Hz sampling """
FFMPEG_PARAMETERS_SAMPLE_48000 = (
FFMPEG_MONO +
FFMPEG_SAMPLE_48000 +
FFMPEG_OVERWRITE +
FFMPEG_PLAIN_HEADER +
FFMPEG_FORMAT_WAVE
)
""" Set of parameters for ``ffmpeg`` with 48000 Hz sampling """
FFMPEG_PARAMETERS_MAP = {
8000: FFMPEG_PARAMETERS_SAMPLE_8000,
16000: FFMPEG_PARAMETERS_SAMPLE_16000,
22050: FFMPEG_PARAMETERS_SAMPLE_22050,
44100: FFMPEG_PARAMETERS_SAMPLE_44100,
48000: FFMPEG_PARAMETERS_SAMPLE_48000
}
""" Map sample rate to parameter list """
FFMPEG_PARAMETERS_DEFAULT = FFMPEG_PARAMETERS_SAMPLE_16000
""" Default set of parameters for ``ffmpeg`` """
TAG = u"FFMPEGWrapper"
[docs] def convert(
self,
input_file_path,
output_file_path,
head_length=None,
process_length=None
):
"""
Convert the audio file at ``input_file_path``
into ``output_file_path``,
using the parameters set in the constructor
or through the ``parameters`` property.
You can skip the beginning of the audio file
by specifying ``head_length`` seconds to skip
(if it is ``None``, start at time zero),
and you can specify to convert
only ``process_length`` seconds
(if it is ``None``, process the entire input file length).
By specifying both ``head_length`` and ``process_length``,
you can skip a portion at the beginning and at the end
of the original input file.
:param string input_file_path: the path of the audio file to convert
:param string output_file_path: the path of the converted audio file
:param float head_length: skip these many seconds
from the beginning of the audio file
:param float process_length: process these many seconds of the audio file
:raises: :class:`~aeneas.ffmpegwrapper.FFMPEGPathError`: if the path to the ``ffmpeg`` executable cannot be called
:raises: OSError: if ``input_file_path`` does not exist
or ``output_file_path`` cannot be written
"""
# test if we can read the input file
if not gf.file_can_be_read(input_file_path):
self.log_exc(u"Input file '%s' cannot be read" % (input_file_path), None, True, OSError)
# test if we can write the output file
if not gf.file_can_be_written(output_file_path):
self.log_exc(u"Output file '%s' cannot be written" % (output_file_path), None, True, OSError)
# call ffmpeg
arguments = [self.rconf[RuntimeConfiguration.FFMPEG_PATH]]
arguments.extend(["-i", input_file_path])
if head_length is not None:
arguments.extend(["-ss", head_length])
if process_length is not None:
arguments.extend(["-t", process_length])
if self.rconf.sample_rate in self.FFMPEG_PARAMETERS_MAP:
arguments.extend(self.FFMPEG_PARAMETERS_MAP[self.rconf.sample_rate])
else:
arguments.extend(self.FFMPEG_PARAMETERS_DEFAULT)
arguments.append(output_file_path)
self.log([u"Calling with arguments '%s'", arguments])
try:
proc = subprocess.Popen(
arguments,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE
)
proc.communicate()
proc.stdout.close()
proc.stdin.close()
proc.stderr.close()
except OSError as exc:
self.log_exc(u"Unable to call the '%s' ffmpeg executable" % (self.rconf[RuntimeConfiguration.FFMPEG_PATH]), exc, True, FFMPEGPathError)
self.log(u"Call completed")
# check if the output file exists
if not gf.file_exists(output_file_path):
self.log_exc(u"Output file '%s' was not written" % (output_file_path), None, True, OSError)
# returning the output file path
self.log([u"Returning output file path '%s'", output_file_path])
return output_file_path