#!/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()