332 lines
9.7 KiB
Python
Executable File
332 lines
9.7 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
import argparse
|
|
import audiofile
|
|
import logging
|
|
from fileops import loggerops
|
|
import pdb
|
|
import os
|
|
import sys
|
|
from database import AudioDatabase, Matcher, Synthesizer
|
|
import config
|
|
import json
|
|
|
|
modpath = sys.argv[0]
|
|
modpath = os.path.splitext(modpath)[0]+'.log'
|
|
|
|
class SmartFormatter(argparse.HelpFormatter):
|
|
"""
|
|
Allows new line to be used in certain help strings.
|
|
|
|
Ref: http://stackoverflow.com/questions/3853722/python-argparse-how-to-insert-newline-in-the-help-text
|
|
"""
|
|
|
|
def _split_lines(self, text, width):
|
|
# this is the RawTextHelpFormatter._split_lines
|
|
if text.startswith('R|'):
|
|
return text[2:].splitlines()
|
|
return argparse.HelpFormatter._split_lines(self, text, width)
|
|
|
|
def parse_sub_args(args, analysis):
|
|
try:
|
|
args = getattr(args, analysis)
|
|
if not args:
|
|
return
|
|
sub_parser = argparse.ArgumentParser()
|
|
|
|
try:
|
|
config_dict = getattr(config, analysis)
|
|
|
|
for item in config_dict:
|
|
sub_parser.add_argument(
|
|
'--{0}'.format(item),
|
|
metavar='',
|
|
type=type(config_dict[item]),
|
|
)
|
|
|
|
sub_args = sub_parser.parse_args(args.split())
|
|
|
|
for item in config_dict.iterkeys():
|
|
argument = getattr(sub_args, item)
|
|
if argument != None:
|
|
config_dict[item] = argument
|
|
setattr(config, analysis, config_dict)
|
|
except AttributeError:
|
|
# If there is no configurations for this analysis
|
|
pass
|
|
except AttributeError:
|
|
# If this analysis' flag is not present in the arguments provided
|
|
pass
|
|
|
|
def parse_arguments():
|
|
"""
|
|
Parses arguments
|
|
Returns a namespace with values for each argument
|
|
"""
|
|
parser = argparse.ArgumentParser(
|
|
description='Concatenator is a tool for synthesizing interpretations of '
|
|
'a sound, through the analysis and synthesis of audio grains from a '
|
|
'corpus database. The program works by analysing overlapping segments of '
|
|
'audio (known as grains) from both the target sound and the source '
|
|
'database, then searching for the closest matching grain in the source '
|
|
'database to the target sound. Finally, the output is generated by '
|
|
'overlap-adding the best matches.',
|
|
formatter_class=SmartFormatter
|
|
)
|
|
|
|
parser.add_argument(
|
|
'source',
|
|
type=str,
|
|
help='Directory of source files/database to take grains from '
|
|
'when synthesizing output'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'target',
|
|
type=str,
|
|
help='Directory of target files/database to match source grains to.'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'output',
|
|
type=str,
|
|
help='Directory to use as database for outputing results and match '
|
|
'information.\nOutput audio will be stored in the /audio sub-directory '
|
|
'and match data will be stored in the /data directory.'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--src_db',
|
|
help="Specifies the directory to create the source database and store analyses "
|
|
"in. If not specified then the source directory will be used directly.",
|
|
type=str,
|
|
metavar=''
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--tar_db',
|
|
help="Specifies the directory to create the target database and store analyses "
|
|
"in. If not specified then the target directory will be used directly.",
|
|
type=str,
|
|
default='',
|
|
metavar=''
|
|
)
|
|
|
|
analyses = [
|
|
"rms",
|
|
"zerox",
|
|
"fft",
|
|
"spccntr",
|
|
"spcsprd",
|
|
"spcflux",
|
|
"spccf",
|
|
"spcflatness",
|
|
"f0",
|
|
"peak",
|
|
"centroid",
|
|
"variance",
|
|
"kurtosis",
|
|
"skewness",
|
|
"harm_ratio"
|
|
]
|
|
|
|
parser.add_argument(
|
|
'--analyse',
|
|
'-a',
|
|
nargs='*',
|
|
help='Specify analyses to be created. Valid analyses are: \'rms\''
|
|
'\'f0\' \'zerox\' \'fft\' etc... (see the documentation for full '
|
|
'details on available analyses)',
|
|
default=analyses
|
|
)
|
|
|
|
helpstrings = {
|
|
"rms": "Overwrite default config setting for rms analysis. Example: \'--window_size 100 --overlap 2\'",
|
|
"fft": "Overwrite default config setting for fft analysis. Example: \'--window_size 2048\'",
|
|
"variance": "Overwrite default config setting for variance analysis. Example: \'--window_size 100 --overlap 2\'",
|
|
"skewness": "Overwrite default config setting for skewness analysis. Example: \'--window_size 100 --overlap 2\'",
|
|
"kurtosis": "Overwrite default config setting for kurtosis analysis. Example: \'--window_size 100 --overlap 2\'",
|
|
"matcher_weightings" : "Set weighting for analysis to set their presedence when matching. Example: \'--f0 2 --rms 1.5\'",
|
|
"analysis_dict" : "Set the formatting of each analysis for grain matching. Example: \'--f0 median --rms mean\'",
|
|
"synthesizer": "Set synthesis settings. Example: \'--enf_rms_ratio_limit 2\'",
|
|
"matcher": "Set matcher settings. Example: \'match_quantity\'"
|
|
}
|
|
|
|
config_items = [item for item in dir(config) if not item.startswith("__") and item in helpstrings.keys()]
|
|
|
|
for item in config_items:
|
|
parser.add_argument(
|
|
'--{0}'.format(item),
|
|
type=str,
|
|
metavar='',
|
|
help=helpstrings[item]
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--reanalyse",
|
|
action="store_true",
|
|
help="Force re-analysis of all analyses, overwriting any existing "
|
|
"analyses"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--rematch",
|
|
action="store_true",
|
|
help="Force re-matching, overwriting any existing match data "
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--enforcef0",
|
|
action="store_true",
|
|
help="This flag enables pitch shifting of matched grains to better match the target."
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--enforcerms",
|
|
action="store_true",
|
|
help="This flag enables scaling of matched grains to better match the target's volume."
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--copy",
|
|
action="store_true",
|
|
help="This flag enables the copying of audio files from their location "
|
|
"to the database, rather than creating symbolic links. This is useful "
|
|
"for creating portable databases."
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--match_method",
|
|
type=str,
|
|
metavar='',
|
|
help="R|Choose the algorithm to use when matching analyses. Available "
|
|
"algorithms are:\nBrute force: \'bruteforce\'\nK-d Tree Search: "
|
|
"'kdtree'",
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--verbose',
|
|
'-v',
|
|
action='count',
|
|
help='Specifies level of verbosity in output. For example: \'-vvvvv\' '
|
|
'will output all information. \'-v\' will output minimal information. '
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
for item in config_items:
|
|
parse_sub_args(args, item)
|
|
|
|
if args.match_method:
|
|
config.matcher["method"] = args.match_method
|
|
|
|
if args.copy:
|
|
config.database["symlink"] = False
|
|
|
|
if args.rematch:
|
|
config.matcher["rematch"] = True
|
|
|
|
if args.reanalyse:
|
|
config.analysis["reanalyse"] = True
|
|
|
|
if args.enforcef0:
|
|
config.synthesizer["enforce_f0"] = True
|
|
|
|
if args.enforcerms:
|
|
config.synthesizer["enforce_rms"] = True
|
|
|
|
if not args.verbose:
|
|
args.verbose = 20
|
|
else:
|
|
levels = [50, 40, 30, 20, 10]
|
|
if args.verbose > 5:
|
|
args.verbose = 5
|
|
args.verbose -= 1
|
|
args.verbose = levels[args.verbose]
|
|
|
|
return args
|
|
|
|
|
|
def main():
|
|
# Process commandline arguments
|
|
args = parse_arguments()
|
|
|
|
src_audio_dir = None
|
|
if args.src_db != '':
|
|
src_audio_dir = args.src_db
|
|
|
|
tar_audio_dir = None
|
|
if args.tar_db != '':
|
|
tar_audio_dir = args.tar_db
|
|
|
|
|
|
logger = loggerops.create_logger(
|
|
logger_streamlevel=args.verbose,
|
|
log_filename=modpath,
|
|
logger_filelevel=args.verbose
|
|
)
|
|
|
|
# Create/load a pre-existing source database
|
|
source_db = AudioDatabase(
|
|
args.source,
|
|
analysis_list=args.analyse,
|
|
config=config,
|
|
db_dir=src_audio_dir
|
|
)
|
|
source_db.load_database(reanalyse=config.analysis["reanalyse"])
|
|
|
|
# Create/load a pre-existing target database
|
|
target_db = AudioDatabase(
|
|
args.target,
|
|
analysis_list=args.analyse,
|
|
config=config,
|
|
db_dir=tar_audio_dir
|
|
)
|
|
target_db.load_database(reanalyse=config.analysis["reanalyse"])
|
|
|
|
# Create/load a pre-existing output database
|
|
output_db = AudioDatabase(
|
|
args.output,
|
|
config=config
|
|
)
|
|
output_db.load_database(reanalyse=False)
|
|
|
|
# Initialise a matching object used for matching the source and target
|
|
# databases.
|
|
matcher = Matcher(
|
|
source_db,
|
|
target_db,
|
|
config.analysis_dict,
|
|
output_db=output_db,
|
|
config=config,
|
|
rematch=args.rematch
|
|
)
|
|
|
|
match_method_dict = {
|
|
'bruteforce': matcher.brute_force_matcher,
|
|
'kdtree': matcher.kdtree_matcher
|
|
}
|
|
|
|
# Perform matching on databases using the method specified.
|
|
matcher.match(
|
|
match_method_dict[config.matcher["method"]],
|
|
grain_size=config.matcher["grain_size"],
|
|
overlap=config.matcher["overlap"]
|
|
)
|
|
|
|
# Initialise a synthesizer object, used for synthesis of the matches.
|
|
synthesizer = Synthesizer(
|
|
source_db,
|
|
output_db,
|
|
target_db=target_db,
|
|
config=config
|
|
)
|
|
|
|
# Perform synthesis.
|
|
synthesizer.synthesize(
|
|
grain_size=config.synthesizer["grain_size"],
|
|
overlap=config.synthesizer["overlap"]
|
|
)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|