| @@ -0,0 +1,408 @@ |
| | 1 | #! /usr/bin/env python |
| | 2 | # -*- coding: utf-8 -*- |
| | 3 | |
| | 4 | """aubio command line tool |
| | 5 | |
| | 6 | This file was written by Paul Brossier <piem@aubio.org> and is released under |
| | 7 | the GNU/GPL v3. |
| | 8 | |
| | 9 | Note: this script is mostly about parsing command line arguments. For more |
| | 10 | readable code examples, check out the `python/demos` folder.""" |
| | 11 | |
| | 12 | import sys |
| | 13 | import argparse |
| | 14 | import aubio |
| | 15 | |
| | 16 | def aubio_parser(): |
| | 17 | epilog = 'use "%(prog)s <command> --help" for more info about each command' |
| | 18 | parser = argparse.ArgumentParser(epilog=epilog) |
| | 19 | parser.add_argument('-V', '--version', help="show version", |
| | 20 | action="store_true", dest="show_version") |
| | 21 | |
| | 22 | subparsers = parser.add_subparsers(title='commands', dest='command', |
| | 23 | metavar="") |
| | 24 | |
| | 25 | # onset subcommand |
| | 26 | subparser = subparsers.add_parser('onset', |
| | 27 | help='estimate time of onsets (beginning of sound event)', |
| | 28 | formatter_class = argparse.ArgumentDefaultsHelpFormatter) |
| | 29 | parser_add_input(subparser) |
| | 30 | parser_add_buf_hop_size(subparser) |
| | 31 | helpstr = "onset novelty function" |
| | 32 | helpstr += " <default|energy|hfc|complex|phase|specdiff|kl|mkl|specflux>" |
| | 33 | parser_add_method(subparser, helpstr=helpstr) |
| | 34 | parser_add_threshold(subparser) |
| | 35 | parser_add_silence(subparser) |
| | 36 | parser_add_minioi(subparser) |
| | 37 | parser_add_time_format(subparser) |
| | 38 | parser_add_verbose_help(subparser) |
| | 39 | subparser.set_defaults(process=process_onset) |
| | 40 | |
| | 41 | # pitch subcommand |
| | 42 | subparser = subparsers.add_parser('pitch', |
| | 43 | help='estimate fundamental frequency (monophonic)') |
| | 44 | parser_add_input(subparser) |
| | 45 | parser_add_buf_hop_size(subparser, buf_size=2048) |
| | 46 | helpstr = "pitch detection method <default|yinfft|yin|mcomb|fcomb|schmitt>" |
| | 47 | parser_add_method(subparser, helpstr=helpstr) |
| | 48 | parser_add_threshold(subparser) |
| | 49 | parser_add_silence(subparser) |
| | 50 | parser_add_time_format(subparser) |
| | 51 | parser_add_verbose_help(subparser) |
| | 52 | subparser.set_defaults(process=process_pitch) |
| | 53 | |
| | 54 | # beat subcommand |
| | 55 | subparser = subparsers.add_parser('beat', |
| | 56 | help='estimate location of beats') |
| | 57 | parser_add_input(subparser) |
| | 58 | parser_add_buf_hop_size(subparser, buf_size=1024, hop_size=512) |
| | 59 | parser_add_time_format(subparser) |
| | 60 | parser_add_verbose_help(subparser) |
| | 61 | subparser.set_defaults(process=process_beat) |
| | 62 | |
| | 63 | # tempo subcommand |
| | 64 | subparser = subparsers.add_parser('tempo', |
| | 65 | help='estimate overall tempo in bpm') |
| | 66 | parser_add_input(subparser) |
| | 67 | parser_add_buf_hop_size(subparser, buf_size=1024, hop_size=512) |
| | 68 | parser_add_time_format(subparser) |
| | 69 | parser_add_verbose_help(subparser) |
| | 70 | subparser.set_defaults(process=process_tempo) |
| | 71 | |
| | 72 | # notes subcommand |
| | 73 | subparser = subparsers.add_parser('notes', |
| | 74 | help='estimate midi-like notes (monophonic)') |
| | 75 | parser_add_input(subparser) |
| | 76 | parser_add_buf_hop_size(subparser) |
| | 77 | parser_add_time_format(subparser) |
| | 78 | parser_add_verbose_help(subparser) |
| | 79 | subparser.set_defaults(process=process_notes) |
| | 80 | |
| | 81 | # mfcc subcommand |
| | 82 | subparser = subparsers.add_parser('mfcc', |
| | 83 | help='extract Mel-Frequency Cepstrum Coefficients') |
| | 84 | parser_add_input(subparser) |
| | 85 | parser_add_buf_hop_size(subparser) |
| | 86 | parser_add_time_format(subparser) |
| | 87 | parser_add_verbose_help(subparser) |
| | 88 | subparser.set_defaults(process=process_mfcc) |
| | 89 | |
| | 90 | # melbands subcommand |
| | 91 | subparser = subparsers.add_parser('melbands', |
| | 92 | help='extract energies in Mel-frequency bands') |
| | 93 | parser_add_input(subparser) |
| | 94 | parser_add_buf_hop_size(subparser) |
| | 95 | parser_add_time_format(subparser) |
| | 96 | parser_add_verbose_help(subparser) |
| | 97 | subparser.set_defaults(process=process_melbands) |
| | 98 | |
| | 99 | return parser |
| | 100 | |
| | 101 | def parser_add_input(parser): |
| | 102 | parser.add_argument("source_uri", default=None, nargs='?', |
| | 103 | help="input sound file to analyse", metavar = "<source_uri>") |
| | 104 | parser.add_argument("-i", "--input", dest = "source_uri2", |
| | 105 | help="input sound file to analyse", metavar = "<source_uri>") |
| | 106 | parser.add_argument("-r", "--samplerate", |
| | 107 | metavar = "<freq>", type=int, |
| | 108 | action="store", dest="samplerate", default=0, |
| | 109 | help="samplerate at which the file should be represented") |
| | 110 | |
| | 111 | def parser_add_verbose_help(parser): |
| | 112 | parser.add_argument("-v","--verbose", |
| | 113 | action="count", dest="verbose", default=1, |
| | 114 | help="make lots of noise [default]") |
| | 115 | parser.add_argument("-q","--quiet", |
| | 116 | action="store_const", dest="verbose", const=0, |
| | 117 | help="be quiet") |
| | 118 | |
| | 119 | def parser_add_buf_hop_size(parser, buf_size=512, hop_size=256): |
| | 120 | parser.add_argument("-B","--bufsize", |
| | 121 | action="store", dest="buf_size", default=buf_size, |
| | 122 | metavar = "<size>", type=int, |
| | 123 | help="buffer size [default=%d]" % buf_size) |
| | 124 | parser.add_argument("-H","--hopsize", |
| | 125 | metavar = "<size>", type=int, |
| | 126 | action="store", dest="hop_size", default=hop_size, |
| | 127 | help="overlap size [default=%d]" % hop_size) |
| | 128 | |
| | 129 | def parser_add_method(parser, method='default', helpstr='method'): |
| | 130 | parser.add_argument("-m","--method", |
| | 131 | metavar = "<method>", type=str, |
| | 132 | action="store", dest="method", default=method, |
| | 133 | help="%s [default=%s]" % (helpstr, method)) |
| | 134 | |
| | 135 | def parser_add_threshold(parser, default=None): |
| | 136 | parser.add_argument("-t","--threshold", |
| | 137 | metavar = "<threshold>", type=float, |
| | 138 | action="store", dest="threshold", default=default, |
| | 139 | help="threshold [default=%s]" % default) |
| | 140 | |
| | 141 | def parser_add_silence(parser): |
| | 142 | parser.add_argument("-s", "--silence", |
| | 143 | metavar = "<value>", type=float, |
| | 144 | action="store", dest="silence", default=-70, |
| | 145 | help="silence threshold") |
| | 146 | |
| | 147 | def parser_add_minioi(parser): |
| | 148 | parser.add_argument("-M", "--minioi", |
| | 149 | metavar = "<value>", type=str, |
| | 150 | action="store", dest="minioi", default="12ms", |
| | 151 | help="minimum Inter-Onset Interval") |
| | 152 | |
| | 153 | def parser_add_time_format(parser): |
| | 154 | helpstr = "select time values output format (samples, ms, seconds)" |
| | 155 | helpstr += " [default=seconds]" |
| | 156 | parser.add_argument("-T", "--time-format", |
| | 157 | metavar='format', |
| | 158 | dest="time_format", |
| | 159 | default=None, |
| | 160 | help=helpstr) |
| | 161 | |
| | 162 | # some utilities |
| | 163 | |
| | 164 | def samples2seconds(n_frames, samplerate): |
| | 165 | return "%f\t" % (n_frames / float(samplerate)) |
| | 166 | |
| | 167 | def samples2milliseconds(n_frames, samplerate): |
| | 168 | return "%f\t" % (1000. * n_frames / float(samplerate)) |
| | 169 | |
| | 170 | def samples2samples(n_frames, samplerate): |
| | 171 | return "%d\t" % n_frames |
| | 172 | |
| | 173 | def timefunc(mode): |
| | 174 | if mode is None or mode == 'seconds' or mode == 's': |
| | 175 | return samples2seconds |
| | 176 | elif mode == 'ms' or mode == 'milliseconds': |
| | 177 | return samples2milliseconds |
| | 178 | elif mode == 'samples': |
| | 179 | return samples2samples |
| | 180 | else: |
| | 181 | raise ValueError('invalid time format %s' % mode) |
| | 182 | |
| | 183 | # definition of processing classes |
| | 184 | |
| | 185 | class default_process(object): |
| | 186 | def __init__(self, args): |
| | 187 | if 'time_format' in args: |
| | 188 | self.time2string = timefunc(args.time_format) |
| | 189 | if args.verbose > 2 and hasattr(self, 'options'): |
| | 190 | name = type(self).__name__.split('_')[1] |
| | 191 | optstr = ' '.join(['running', name, 'with options', repr(self.options), '\n']) |
| | 192 | sys.stderr.write(optstr) |
| | 193 | def flush(self, n_frames, samplerate): |
| | 194 | # optionally called at the end of process |
| | 195 | pass |
| | 196 | |
| | 197 | def parse_options(self, args, valid_opts): |
| | 198 | # get any valid options found in a dictionnary of arguments |
| | 199 | options = {k :v for k,v in vars(args).items() if k in valid_opts} |
| | 200 | self.options = options |
| | 201 | |
| | 202 | def remap_pvoc_options(self, options): |
| | 203 | # FIXME: we need to remap buf_size to win_s, hop_size to hop_s |
| | 204 | # adjust python/ext/py-phasevoc.c to understand buf_size/hop_size |
| | 205 | if 'buf_size' in options: |
| | 206 | options['win_s'] = options['buf_size'] |
| | 207 | del options['buf_size'] |
| | 208 | if 'hop_size' in options: |
| | 209 | options['hop_s'] = options['hop_size'] |
| | 210 | del options['hop_size'] |
| | 211 | self.options = options |
| | 212 | |
| | 213 | class process_onset(default_process): |
| | 214 | valid_opts = ['method', 'hop_size', 'buf_size', 'samplerate'] |
| | 215 | def __init__(self, args): |
| | 216 | self.parse_options(args, self.valid_opts) |
| | 217 | self.onset = aubio.onset(**self.options) |
| | 218 | if args.threshold is not None: |
| | 219 | self.onset.set_threshold(args.threshold) |
| | 220 | if args.minioi: |
| | 221 | if args.minioi.endswith('ms'): |
| | 222 | self.onset.set_minioi_ms(float(args.minioi[:-2])) |
| | 223 | elif args.minioi.endswith('s'): |
| | 224 | self.onset.set_minioi_s(float(args.minioi[:-1])) |
| | 225 | else: |
| | 226 | self.onset.set_minioi(int(args.minioi)) |
| | 227 | if args.silence: |
| | 228 | self.onset.set_silence(args.silence) |
| | 229 | super(process_onset, self).__init__(args) |
| | 230 | def __call__(self, block): |
| | 231 | return self.onset(block) |
| | 232 | def repr_res(self, res, frames_read, samplerate): |
| | 233 | if res[0] != 0: |
| | 234 | outstr = self.time2string(self.onset.get_last(), samplerate) |
| | 235 | sys.stdout.write(outstr + '\n') |
| | 236 | |
| | 237 | class process_pitch(default_process): |
| | 238 | valid_opts = ['method', 'hop_size', 'buf_size', 'samplerate'] |
| | 239 | def __init__(self, args): |
| | 240 | self.parse_options(args, self.valid_opts) |
| | 241 | self.pitch = aubio.pitch(**self.options) |
| | 242 | if args.threshold is not None: |
| | 243 | self.pitch.set_tolerance(args.threshold) |
| | 244 | if args.silence is not None: |
| | 245 | self.pitch.set_silence(args.silence) |
| | 246 | super(process_pitch, self).__init__(args) |
| | 247 | def __call__(self, block): |
| | 248 | return self.pitch(block) |
| | 249 | def repr_res(self, res, frames_read, samplerate): |
| | 250 | fmt_out = self.time2string(frames_read, samplerate) |
| | 251 | sys.stdout.write(fmt_out + "%.6f\n" % res[0]) |
| | 252 | |
| | 253 | class process_beat(default_process): |
| | 254 | valid_opts = ['method', 'hop_size', 'buf_size', 'samplerate'] |
| | 255 | def __init__(self, args): |
| | 256 | self.parse_options(args, self.valid_opts) |
| | 257 | self.tempo = aubio.tempo(**self.options) |
| | 258 | super(process_beat, self).__init__(args) |
| | 259 | def __call__(self, block): |
| | 260 | return self.tempo(block) |
| | 261 | def repr_res(self, res, frames_read, samplerate): |
| | 262 | if res[0] != 0: |
| | 263 | outstr = self.time2string(self.tempo.get_last(), samplerate) |
| | 264 | sys.stdout.write(outstr + '\n') |
| | 265 | |
| | 266 | class process_tempo(process_beat): |
| | 267 | def __init__(self, args): |
| | 268 | super(process_tempo, self).__init__(args) |
| | 269 | self.beat_locations = [] |
| | 270 | def repr_res(self, res, frames_read, samplerate): |
| | 271 | if res[0] != 0: |
| | 272 | self.beat_locations.append(self.tempo.get_last_s()) |
| | 273 | def flush(self, frames_read, samplerate): |
| | 274 | import numpy as np |
| | 275 | if len(self.beat_locations) < 2: |
| | 276 | outstr = "unknown bpm" |
| | 277 | else: |
| | 278 | bpms = 60./ np.diff(self.beat_locations) |
| | 279 | median_bpm = np.mean(bpms) |
| | 280 | if len(self.beat_locations) < 10: |
| | 281 | outstr = "%.2f bpm (uncertain)" % median_bpm |
| | 282 | else: |
| | 283 | outstr = "%.2f bpm" % median_bpm |
| | 284 | sys.stdout.write(outstr + '\n') |
| | 285 | |
| | 286 | class process_notes(default_process): |
| | 287 | valid_opts = ['method', 'hop_size', 'buf_size', 'samplerate'] |
| | 288 | def __init__(self, args): |
| | 289 | self.parse_options(args, self.valid_opts) |
| | 290 | self.notes = aubio.notes(**self.options) |
| | 291 | super(process_notes, self).__init__(args) |
| | 292 | def __call__(self, block): |
| | 293 | return self.notes(block) |
| | 294 | def repr_res(self, res, frames_read, samplerate): |
| | 295 | if res[2] != 0: # note off |
| | 296 | fmt_out = self.time2string(frames_read, samplerate) |
| | 297 | sys.stdout.write(fmt_out + '\n') |
| | 298 | if res[0] != 0: # note on |
| | 299 | lastmidi = res[0] |
| | 300 | fmt_out = "%f\t" % lastmidi |
| | 301 | fmt_out += self.time2string(frames_read, samplerate) |
| | 302 | sys.stdout.write(fmt_out) # + '\t') |
| | 303 | def flush(self, frames_read, samplerate): |
| | 304 | eof = self.time2string(frames_read, samplerate) |
| | 305 | sys.stdout.write(eof + '\n') |
| | 306 | |
| | 307 | class process_mfcc(default_process): |
| | 308 | def __init__(self, args): |
| | 309 | valid_opts1 = ['hop_size', 'buf_size'] |
| | 310 | self.parse_options(args, valid_opts1) |
| | 311 | self.remap_pvoc_options(self.options) |
| | 312 | self.pv = aubio.pvoc(**self.options) |
| | 313 | |
| | 314 | valid_opts2 = ['buf_size', 'n_filters', 'n_coeffs', 'samplerate'] |
| | 315 | self.parse_options(args, valid_opts2) |
| | 316 | self.mfcc = aubio.mfcc(**self.options) |
| | 317 | |
| | 318 | # remember all options |
| | 319 | self.parse_options(args, list(set(valid_opts1 + valid_opts2))) |
| | 320 | |
| | 321 | super(process_mfcc, self).__init__(args) |
| | 322 | |
| | 323 | def __call__(self, block): |
| | 324 | fftgrain = self.pv(block) |
| | 325 | return self.mfcc(fftgrain) |
| | 326 | def repr_res(self, res, frames_read, samplerate): |
| | 327 | fmt_out = self.time2string(frames_read, samplerate) |
| | 328 | fmt_out += ' '.join(["% 9.7f" % f for f in res.tolist()]) |
| | 329 | sys.stdout.write(fmt_out + '\n') |
| | 330 | |
| | 331 | class process_melbands(default_process): |
| | 332 | def __init__(self, args): |
| | 333 | self.args = args |
| | 334 | valid_opts = ['hop_size', 'buf_size'] |
| | 335 | self.parse_options(args, valid_opts) |
| | 336 | self.remap_pvoc_options(self.options) |
| | 337 | self.pv = aubio.pvoc(**self.options) |
| | 338 | |
| | 339 | valid_opts = ['buf_size', 'n_filters'] |
| | 340 | self.parse_options(args, valid_opts) |
| | 341 | self.remap_pvoc_options(self.options) |
| | 342 | self.filterbank = aubio.filterbank(**self.options) |
| | 343 | self.filterbank.set_mel_coeffs_slaney(args.samplerate) |
| | 344 | |
| | 345 | super(process_melbands, self).__init__(args) |
| | 346 | def __call__(self, block): |
| | 347 | fftgrain = self.pv(block) |
| | 348 | return self.filterbank(fftgrain) |
| | 349 | def repr_res(self, res, frames_read, samplerate): |
| | 350 | fmt_out = self.time2string(frames_read, samplerate) |
| | 351 | fmt_out += ' '.join(["% 9.7f" % f for f in res.tolist()]) |
| | 352 | sys.stdout.write(fmt_out + '\n') |
| | 353 | |
| | 354 | def main(): |
| | 355 | parser = aubio_parser() |
| | 356 | args = parser.parse_args() |
| | 357 | if 'show_version' in args and args.show_version: |
| | 358 | sys.stdout.write('aubio version ' + aubio.version + '\n') |
| | 359 | sys.exit(0) |
| | 360 | elif 'verbose' in args and args.verbose > 3: |
| | 361 | sys.stderr.write('aubio version ' + aubio.version + '\n') |
| | 362 | if 'command' not in args or args.command is None: |
| | 363 | # no command given, print help and return 1 |
| | 364 | parser.print_help() |
| | 365 | sys.exit(1) |
| | 366 | elif not args.source_uri and not args.source_uri2: |
| | 367 | sys.stderr.write("Error: a source is required\n") |
| | 368 | parser.print_help() |
| | 369 | sys.exit(1) |
| | 370 | elif args.source_uri2 is not None: |
| | 371 | args.source_uri = args.source_uri2 |
| | 372 | try: |
| | 373 | # open source_uri |
| | 374 | with aubio.source(args.source_uri, hop_size=args.hop_size, |
| | 375 | samplerate=args.samplerate) as a_source: |
| | 376 | # always update args.samplerate to native samplerate, in case |
| | 377 | # source was opened with args.samplerate=0 |
| | 378 | args.samplerate = a_source.samplerate |
| | 379 | # create the processor for this subcommand |
| | 380 | processor = args.process(args) |
| | 381 | frames_read = 0 |
| | 382 | while True: |
| | 383 | # read new block from source |
| | 384 | block, read = a_source() |
| | 385 | # execute processor on this block |
| | 386 | res = processor(block) |
| | 387 | # print results for this block |
| | 388 | if args.verbose > 0: |
| | 389 | processor.repr_res(res, frames_read, a_source.samplerate) |
| | 390 | # increment total number of frames read |
| | 391 | frames_read += read |
| | 392 | # exit loop at end of file |
| | 393 | if read < a_source.hop_size: break |
| | 394 | # flush the processor if needed |
| | 395 | processor.flush(frames_read, a_source.samplerate) |
| | 396 | if args.verbose > 1: |
| | 397 | fmt_string = "read {:.2f}s" |
| | 398 | fmt_string += " ({:d} samples in {:d} blocks of {:d})" |
| | 399 | fmt_string += " from {:s} at {:d}Hz\n" |
| | 400 | sys.stderr.write(fmt_string.format( |
| | 401 | frames_read/float(a_source.samplerate), |
| | 402 | frames_read, |
| | 403 | frames_read // a_source.hop_size + 1, |
| | 404 | a_source.hop_size, |
| | 405 | a_source.uri, |
| | 406 | a_source.samplerate)) |
| | 407 | except KeyboardInterrupt: |
| | 408 | sys.exit(1) |