Source code for sox.combine
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Python wrapper around the SoX library.
This module requires that SoX is installed.
'''
from __future__ import print_function
import logging
from . import file_info
from . import core
from .core import sox
from .core import SoxError
from .transform import Transformer
logging.basicConfig(level=logging.DEBUG)
COMBINE_VALS = [
'concatenate', 'merge', 'mix', 'mix-power', 'multiply'
]
[docs]class Combiner(Transformer):
'''Audio file combiner.
Class which allows multiple files to be combined to create an output
file, saved to output_filepath.
Inherits all methods from the Transformer class, thus any effects can be
applied after combining.
Parameters
----------
input_filepath_list : list of str
List of paths to input audio files.
output_filepath : str
Path to desired output file. If a file already exists at the given
path, the file will be overwritten.
combine_type : str
Input file combining method. One of the following values:
* concatenate : combine input files by concatenating in the
order given.
* merge : combine input files by stacking each input file into
a new channel of the output file.
* mix : combine input files by summing samples in corresponding
channels.
* mix-power : combine input files with volume adjustments such
that the output volume is roughly equivlent to one of the
input signals.
* multiply : combine input files by multiplying samples in
corresponding samples.
input_volumes : list of float, default=None
List of volumes to be applied upon combining input files. Volumes
are applied to the input files in order.
If None, input files will be combined at their original volumes.
'''
def __init__(self, input_filepath_list, output_filepath,
combine_type, input_volumes=None):
file_info.validate_input_file_list(input_filepath_list)
file_info.validate_output_file(output_filepath)
_validate_combine_type(combine_type)
_validate_volumes(input_volumes)
super(Combiner, self).__init__(input_filepath_list[0], output_filepath)
self.combine = ['--combine', combine_type]
self.input_filepath_list = input_filepath_list
self._validate_file_formats()
self.input_format_list = []
self._set_input_format_list(input_volumes)
self.input_args = []
def _validate_file_formats(self):
'''Validate that combine method can be performed with given files.
Raises IOError if input file formats are incompatible.
'''
self._validate_sample_rates()
if self.combine == ['--combine', 'concatenate']:
self._validate_num_channels()
def _validate_sample_rates(self):
''' Check if files in input file list have the same sample rate
'''
sample_rates = [
file_info.sample_rate(f) for f in self.input_filepath_list
]
if not core.all_equal(sample_rates):
raise IOError(
"Input files do not have the same sample rate. The {} combine "
"type requires that all files have the same sample rate"
.format(self.combine)
)
def _validate_num_channels(self):
''' Check if files in input file list have the same number of channels
'''
channels = [
file_info.channels(f) for f in self.input_filepath_list
]
if not core.all_equal(channels):
raise IOError(
"Input files do not have the same number of channels. The "
"{} combine type requires that all files have the same "
"number of channels"
.format(self.combine)
)
def _set_input_format_list(self, input_volumes):
'''Set input formats given input_volumes.
Parameters
----------
input_volumes : list of float, default=None
List of volumes to be applied upon combining input files. Volumes
are applied to the input files in order.
If None, input files will be combined at their original volumes.
'''
n_inputs = len(self.input_filepath_list)
input_format_list = []
if input_volumes is None:
vols = [1] * n_inputs
else:
n_volumes = len(input_volumes)
if n_volumes < n_inputs:
logging.warning(
'Volumes were only specified for %s out of %s files.'
'The last %s files will remain at their original volumes.',
n_volumes, n_inputs, n_inputs - n_volumes
)
vols = input_volumes + [1] * (n_inputs - n_volumes)
elif n_volumes > n_inputs:
logging.warning(
'%s volumes were specified but only %s input files exist.'
'The last %s volumes will be ignored.',
n_volumes, n_inputs, n_volumes - n_inputs
)
vols = input_volumes[:n_inputs]
else:
vols = [v for v in input_volumes]
for vol in vols:
input_format_list.append(['-v', '{}'.format(vol)])
self.input_format_list = input_format_list
def _build_input_args(self):
''' Builds input arguments by stitching input filepaths and input
formats together.
'''
if len(self.input_format_list) != len(self.input_filepath_list):
raise ValueError(
"input_format_list & input_filepath_list are not the same size"
)
input_args = []
zipped = zip(self.input_filepath_list, self.input_format_list)
for input_file, input_fmt in zipped:
input_args.extend(input_fmt)
input_args.append(input_file)
self.input_args = input_args
[docs] def build(self):
''' Executes SoX. '''
args = []
args.extend(self.globals)
args.extend(self.combine)
self._build_input_args()
args.extend(self.input_args)
args.extend(self.output_format)
args.append(self.output_filepath)
args.extend(self.effects)
status = sox(args)
if status is False:
raise SoxError
logging.info(
"Created %s with combiner %s and effects: %s",
self.output_filepath,
self.combine,
" ".join(self.effects_log)
)
return True
def _validate_combine_type(combine_type):
'''Check that the combine_type is valid.
Parameters
----------
combine_type : str
Combine type.
'''
if combine_type not in COMBINE_VALS:
raise ValueError(
'Invalid value for combine_type. Must be one of {}'.format(
COMBINE_VALS)
)
def _validate_volumes(input_volumes):
'''Check input_volumes contains a valid list of volumes.
Parameters
----------
input_volumes : list
list of volume values. Castable to numbers.
'''
if not (input_volumes is None or isinstance(input_volumes, list)):
raise TypeError("input_volumes must be None or a list.")
if isinstance(input_volumes, list):
for vol in input_volumes:
if not core.is_number(vol):
raise ValueError(
"Elements of input_volumes must be numbers: found {}"
.format(vol)
)