Package echonest :: Module audio
[hide private]
[frames] | no frames]

Source Code for Module echonest.audio

   1  """ 
   2  The main `Echo Nest`_ `Remix API`_ module for manipulating audio files and  
   3  their associated `Echo Nest`_ `Analyze API`_ analyses. 
   4   
   5  AudioData, and getpieces by Robert Ochshorn 
   6  on 2008-06-06.  Some refactoring and everything else by Joshua Lifton 
   7  2008-09-07.  Refactoring by Ben Lacker 2009-02-11. Other contributions 
   8  by Adam Lindsay. 
   9   
  10  :group Base Classes: AudioAnalysis, AudioData 
  11  :group Audio-plus-Analysis Classes: AudioFile, LocalAudioFile, ExistingTrack, LocalAnalysis 
  12  :group Building Blocks: AudioQuantum, AudioSegment, AudioQuantumList 
  13   
  14  :group Audio helper functions: getpieces, mix 
  15  :group Utility functions: chain_from_mixed 
  16  :group Parsers: globalParserFloat, globalParserInt, *Parser 
  17   
  18  .. _Analyze API: http://developer.echonest.com/pages/overview?version=2 
  19  .. _Remix API: http://code.google.com/p/echo-nest-remix/ 
  20  .. _Echo Nest: http://the.echonest.com/ 
  21  """ 
  22   
  23  __version__ = "$Revision: 0 $" 
  24  # $Source$ 
  25   
  26  import aifc 
  27  import commands 
  28  import md5 
  29  import numpy 
  30  import os 
  31  import sys 
  32  import StringIO 
  33  import struct 
  34  import tempfile 
  35  import wave 
  36  import lame 
  37  import mad 
  38  import echonest.web.analyze as analyze; 
  39  import echonest.web.util as util 
  40  import echonest.web.config as config 
  41   
  42  import echonest.selection as selection 
  43   
  44   
45 -class AudioAnalysis(object) :
46 """ 47 This class wraps `echonest.web` to allow transparent caching of the 48 audio analysis of an audio file. 49 50 For example, the following script will display the bars of a track 51 twice:: 52 53 from echonest import * 54 a = audio.AudioAnalysis('YOUR_TRACK_ID_HERE') 55 a.bars 56 a.bars 57 58 The first time `a.bars` is called, a network request is made of the 59 `Echo Nest`_ `Analyze API`_. The second time time `a.bars` is called, the 60 cached value is returned immediately. 61 62 An `AudioAnalysis` object can be created using an existing ID, as in 63 the example above, or by specifying the audio file to upload in 64 order to create the ID, as in:: 65 66 a = audio.AudioAnalysis(filename='FULL_PATH_TO_AUDIO_FILE') 67 68 .. _Analyze API: http://developer.echonest.com/pages/overview?version=2 69 .. _Echo Nest: http://the.echonest.com/ 70 """ 71 72 #: Any variable in this listing is fetched over the network once 73 #: and then cached. Calling refreshCachedVariables will force a 74 #: refresh. 75 CACHED_VARIABLES = ( 'bars', 76 'beats', 77 'duration', 78 'end_of_fade_in', 79 'key', 80 'loudness', 81 'metadata', 82 'mode', 83 'sections', 84 'segments', 85 'start_of_fade_out', 86 'tatums', 87 'tempo', 88 'time_signature' ) 89
90 - def __init__( self, audio, parsers=None ) :
91 """ 92 Constructor. If the argument is a valid local path or a URL, 93 the track ID is generated by uploading the file to the `Echo Nest`_ 94 `Analyze API`_\. Otherwise, the argument is assumed to be 95 the track ID. 96 97 :param audio: A string representing either a path to a local 98 file, a valid URL, or the ID of a file that has already 99 been uploaded for analysis. 100 101 :param parsers: A dictionary of keys consisting of cached 102 variable names and values consisting of functions to 103 be used to parse those variables as they are cached. 104 No parsing is done for variables without parsing functions 105 or if the parsers argument is None. 106 107 .. _Analyze API: http://developer.echonest.com/pages/overview?version=2 108 .. _Echo Nest: http://the.echonest.com/ 109 """ 110 111 if parsers is None : 112 parsers = {} 113 self.parsers = parsers 114 115 if type(audio) is not str : 116 # Argument is invalid. 117 raise TypeError("Argument 'audio' must be a string representing either a filename, track ID, or MD5.") 118 elif os.path.isfile(audio) or '.' in audio : 119 # Argument is either a filename or URL. 120 doc = analyze.upload(audio) 121 self.id = doc.getElementsByTagName('thingID')[0].firstChild.data 122 else: 123 # Argument is a md5 or track ID. 124 self.id = audio 125 126 # Initialize cached variables to None. 127 for cachedVar in AudioAnalysis.CACHED_VARIABLES : 128 self.__setattr__(cachedVar, None)
129
130 - def refreshCachedVariables( self ) :
131 """ 132 Forces all cached variables to be updated over the network. 133 """ 134 for cachedVar in AudioAnalysis.CACHED_VARIABLES : 135 self.__setattr__(cachedVar, None) 136 self.__getattribute__(cachedVar)
137
138 - def __getattribute__( self, name ) :
139 """ 140 This function has been modified to support caching of 141 variables retrieved over the network. As a result, each 142 of the `CACHED_VARIABLES` is available as an accessor. 143 """ 144 if name in AudioAnalysis.CACHED_VARIABLES : 145 if object.__getattribute__(self, name) is None : 146 getter = analyze.__dict__[ 'get_' + name ] 147 value = getter(object.__getattribute__(self, 'id')) 148 parseFunction = object.__getattribute__(self, 'parsers').get(name) 149 if parseFunction : 150 value = parseFunction(value) 151 self.__setattr__(name, value) 152 if type(object.__getattribute__(self, name)) == AudioQuantumList: 153 object.__getattribute__(self, name).attach(self) 154 return object.__getattribute__(self, name)
155
156 - def __setstate__(self, state):
157 """ 158 Recreates circular references after unpickling. 159 """ 160 self.__dict__.update(state) 161 for cached_var in AudioAnalysis.CACHED_VARIABLES: 162 if type(object.__getattribute__(self, cached_var)) == AudioQuantumList: 163 object.__getattribute__(self, cached_var).attach(self)
164 165 166
167 -class AudioData(object):
168 """ 169 Handles audio data transparently. A smart audio container 170 with accessors that include: 171 172 sampleRate 173 samples per second 174 numChannels 175 number of channels 176 data 177 a `numpy.array`_ 178 179 .. _numpy.array: http://docs.scipy.org/doc/numpy/reference/generated/numpy.array.html 180 """
181 - def __init__(self, filename=None, ndarray = None, shape=None, sampleRate=None, numChannels=None):
182 """ 183 Given an input `ndarray`, import the sample values and shape 184 (if none is specified) of the input `numpy.array`. 185 186 Given a `filename` (and no input ndarray), load the MP3, WAVE or 187 AIFF file into the data, auto-detecting the file extension, 188 sample rate, and number of channels. 189 190 :param filename: a path to an audio file for loading its sample 191 data into the AudioData.data 192 :param ndarray: a `numpy.array`_ instance with sample data 193 :param shape: a tuple of array dimensions 194 :param sampleRate: sample rate, in Hz 195 :param numChannels: number of channels 196 197 .. _numpy.array: http://docs.scipy.org/doc/numpy/reference/generated/numpy.array.html 198 """ 199 if (filename is not None) and (ndarray is None) : 200 if sampleRate is None or numChannels is None: 201 #force sampleRate and num numChannels to 44100 hz, 2 202 sampleRate, numChannels = 44100, 2 203 204 filelower = filename.lower() 205 206 if filelower.endswith('.mp3'): 207 mf = mad.MadFile(filename) 208 buf = StringIO.StringIO() 209 data = True 210 while data: 211 data = mf.read() 212 if data is not None: 213 buf.write(data) 214 audiodata = numpy.fromstring(buf.getvalue(),dtype=numpy.int16) 215 else: 216 if filelower.endswith('.wav'): 217 f = wave.open(filename, 'r') 218 elif filelower.endswith('.aiff'): 219 f = aifc.open(filename, 'r') 220 numFrames = f.getnframes() 221 raw = f.readframes(numFrames) 222 numChannels = f.getnchannels() 223 sampleSize = numFrames*numChannels 224 audiodata = numpy.array(map(int,struct.unpack("%sh" %sampleSize,raw)), numpy.int16) 225 ndarray = numpy.array(audiodata, dtype=numpy.int16) 226 ndarray = audiodata.reshape(len(audiodata)/numChannels,numChannels) 227 228 # Continue with the old __init__() function 229 self.filename = filename 230 self.sampleRate = sampleRate 231 self.numChannels = numChannels 232 233 if shape is None and isinstance(ndarray, numpy.ndarray): 234 self.data = numpy.zeros(ndarray.shape, dtype=numpy.int16) 235 elif shape is not None: 236 self.data = numpy.zeros(shape, dtype=numpy.int16) 237 else: 238 self.data = None 239 self.endindex = 0 240 if ndarray is not None: 241 self.endindex = len(ndarray) 242 self.data[0:self.endindex] = ndarray
243
244 - def __getitem__(self, index):
245 """ 246 Fetches a frame or slice. Returns an individual frame (if the index 247 is a time offset float or an integer sample number) or a slice if 248 the index is an `AudioQuantum` (or quacks like one). 249 """ 250 if isinstance(index, float): 251 index = int(index*self.sampleRate) 252 elif hasattr(index, "start") and hasattr(index, "duration"): 253 index = slice(index.start, index.start+index.duration) 254 255 if isinstance(index, slice): 256 if ( hasattr(index.start, "start") and 257 hasattr(index.stop, "duration") and 258 hasattr(index.stop, "start") ) : 259 index = slice(index.start.start, index.stop.start+index.stop.duration) 260 261 if isinstance(index, slice): 262 return self.getslice(index) 263 else: 264 return self.getsample(index)
265
266 - def getslice(self, index):
267 "Help `__getitem__` return a new AudioData for a given slice" 268 if isinstance(index.start, float): 269 index = slice(int(index.start*self.sampleRate), int(index.stop*self.sampleRate), index.step) 270 return AudioData(None, self.data[index],sampleRate=self.sampleRate)
271
272 - def getsample(self, index):
273 """ 274 Help `__getitem__` return a frame (all channels for a given 275 sample index) 276 """ 277 if isinstance(index, int): 278 return self.data[index] 279 else: 280 #let the numpy array interface be clever 281 return AudioData(None, self.data[index])
282
283 - def __add__(self, as2):
284 """ 285 Returns a new `AudioData` from the concatenation of the two arguments. 286 """ 287 if self.data is None: 288 return AudioData(None, as2.data.copy()) 289 elif as2.data is None: 290 return AudioData(None, self.data.copy()) 291 else: 292 return AudioData(None, numpy.concatenate((self.data,as2.data)))
293
294 - def append(self, as2):
295 "Appends the input to the end of this `AudioData`." 296 self.data[self.endindex:self.endindex+len(as2)] = as2.data[0:] 297 self.endindex += len(as2)
298
299 - def __len__(self):
300 if self.data is not None: 301 return len(self.data) 302 else: 303 return 0
304
305 - def encode(self, filename):
306 """ 307 Output an MP3 or WAVE file according to the `filename`\'s extension. 308 If no appropriate extension is given, then force an MP3 output. 309 """ 310 filelower = filename.lower() 311 if filelower.endswith(".mp3"): 312 self.encode_mp3(filename) 313 elif filelower.endswith(".wav"): 314 self.encode_wav(filename) 315 else: 316 # The user has likely waited a long time by now. If a bad filename 317 # had not been verified by the program earlier, we may as well be 318 # kind enough give the user something for their efforts. 319 print >> sys.stderr, "Unknown file extension, saving as mp3" 320 self.encode_mp3(filename + ".mp3")
321
322 - def encode_mp3(self, mp3_path):
323 "Help `encode`\() write out an MP3 file via LAME." 324 sampwidth = 2 325 num_channels = self.numChannels 326 try: 327 bitrate = config.MP3_BITRATE 328 except NameError: 329 bitrate = 128 330 if num_channels == 1: 331 #if it's a mono, force it to be stereo. 332 # Can be removed when py-lame implements "encode" function 333 num_channels = 2 334 # make a new stereo array out of the mono array 335 data_stereo = numpy.column_stack((self.data[:,numpy.newaxis],self.data[:,numpy.newaxis])) 336 nframes = len(self.data) / num_channels 337 raw_size = num_channels * sampwidth * nframes 338 mp3_file = open(mp3_path, "wb+") 339 mp3 = lame.init() 340 mp3.set_num_channels(num_channels) 341 mp3.set_in_samplerate(self.sampleRate) 342 mp3.set_num_samples(long(nframes)) 343 mp3.set_bitrate(bitrate) 344 mp3.init_parameters() 345 # 1 sample = 2 bytes 346 num_samples_per_enc_run = self.sampleRate 347 num_bytes_per_enc_run = num_channels * num_samples_per_enc_run * sampwidth 348 start = 0 349 while True: 350 if self.numChannels == 1: 351 #if mono use the new stereo from dual mono array 352 frames = data_stereo[start:start+num_samples_per_enc_run].tostring() 353 else: 354 #if not use the stereo array 355 frames = self.data[start:start+num_samples_per_enc_run].tostring() 356 data = mp3.encode_interleaved(frames) 357 mp3_file.write(data) 358 start = start + num_samples_per_enc_run 359 if start >= len(self.data): break 360 mp3_file.write(mp3.flush_buffers()) 361 mp3.write_tags(mp3_file) 362 mp3_file.close() 363 mp3.delete()
364
365 - def encode_wav(self, filename=None):
366 "Help `encode`\() write out a WAVE file." 367 368 if filename is None: 369 foo,filename = tempfile.mkstemp(".wav") 370 371 ###BASED ON SCIPY SVN (http://projects.scipy.org/pipermail/scipy-svn/2007-August/001189.html)### 372 fid = open(filename, 'wb') 373 fid.write('RIFF') 374 fid.write('\x00\x00\x00\x00') 375 fid.write('WAVE') 376 # fmt chunk 377 fid.write('fmt ') 378 if self.data.ndim == 1: 379 noc = 1 380 else: 381 noc = self.data.shape[1] 382 bits = self.data.dtype.itemsize * 8 383 sbytes = self.sampleRate*(bits / 8)*noc 384 ba = noc * (bits / 8) 385 fid.write(struct.pack('lhHLLHH', 16, 1, noc, self.sampleRate, sbytes, ba, bits)) 386 # data chunk 387 fid.write('data') 388 fid.write(struct.pack('l', self.data.nbytes)) 389 self.data.tofile(fid) 390 # Determine file size and place it in correct 391 # position at start of the file. 392 size = fid.tell() 393 fid.seek(4) 394 fid.write(struct.pack('l', size-8)) 395 fid.close() 396 397 return filename
398 399 400 401
402 -def getpieces(audioData, segs):
403 """ 404 Collects audio samples for output. 405 Returns a new `AudioData` where the new sample data is assembled 406 from the input audioData according to the time offsets in each 407 of the elements of the input segs (commonly an `AudioQuantumList`). 408 409 :param audioData: an `AudioData` object 410 :param segs: an iterable containing objects that may be accessed 411 as slices or indices for an `AudioData` 412 """ 413 #calculate length of new segment 414 dur = 0 415 for s in segs: 416 dur += int(s.duration*audioData.sampleRate) 417 418 dur += 100000 #another two seconds just for goodwill... 419 420 #determine shape of new array 421 if len(audioData.data.shape) > 1: 422 newshape = (dur, audioData.data.shape[1]) 423 newchans = audioData.data.shape[1] 424 else: 425 newshape = (dur,) 426 newchans = 1 427 428 #make accumulator segment 429 newAD = AudioData(shape=newshape,sampleRate=audioData.sampleRate, numChannels=newchans) 430 431 #concatenate segs to the new segment 432 for s in segs: 433 newAD.append(audioData[s]) 434 435 return newAD
436
437 -def mix(audioDataA,audioDataB,mix=0.5):
438 """ 439 Mixes two `AudioData` objects. Assumes they have the same sample rate 440 and number of channels. 441 442 Mix takes a float 0-1 and determines the relative mix of two audios. 443 i.e., mix=0.9 yields greater presence of audioDataA in the final mix. 444 """ 445 audioDataA.data *= float(mix) 446 audioDataB.data *= 1-float(mix) 447 if audioDataA.endindex > audioDataB.endindex: 448 audioDataA.data[:audioDataB.endindex] += audioDataB.data[0:] 449 return audioDataA 450 elif audioDataB.endindex > audioDataA.endindex: 451 audioDataB.data[:audioDataA.endindex] += audioDataA.data[0:] 452 return audioDataB 453 elif audioDataA.endindex == audioDataB.endindex: 454 audioDataA.data[:] += audioDataB.data[:] 455 return audioDataA
456 457
458 -class AudioFile(AudioData) :
459 """ 460 The basic do-everything class for remixing. Acts as an `AudioData` 461 object, but with an added `analysis` selector which is an 462 `AudioAnalysis` object. 463 """
464 - def __init__(self, filename) :
465 """ 466 :param filename: path to a local MP3 file 467 """ 468 AudioData.__init__(self, filename=filename) 469 self.analysis = AudioAnalysis(filename, PARSERS)
470 471 472
473 -class ExistingTrack(object):
474 """ 475 Analysis only (under the `analysis` selector), with a local file 476 known to be already analyzed by The `Echo Nest`_\'s servers. 477 478 .. _Echo Nest: http://the.echonest.com/ 479 """
480 - def __init__(self, trackID_or_Filename):
481 """ 482 :param trackID_or_Filename: a path to a local MP3 file or a 483 valid Echo Nest `track identifier`_ 484 485 .. _track identifier: http://developer.echonest.com/docs/datatypes/?version=2#track_id 486 """ 487 if(os.path.isfile(trackID_or_Filename)): 488 trackID = md5.new(file(trackID_or_Filename).read()).hexdigest() 489 print >> sys.stderr, "Computed MD5 of file is " + trackID 490 else: 491 trackID = trackID_or_Filename 492 self.analysis = AudioAnalysis(trackID, PARSERS)
493 494
495 -class LocalAudioFile(AudioFile):
496 """ 497 Like `AudioFile`, but with conditional upload: recommended. If a file 498 is already known to the Analyze API, then it does not bother uploading 499 the file. 500 """
501 - def __init__(self, filename):
502 """ 503 :param filename: path to a local MP3 file 504 """ 505 trackID = md5.new(file(filename).read()).hexdigest() 506 print >> sys.stderr, "Computed MD5 of file is " + trackID 507 try: 508 print >> sys.stderr, "Probing for existing analysis" 509 tempanalysis = AudioAnalysis(trackID, {'duration': globalParserFloat}) 510 tempanalysis.duration 511 self.analysis = AudioAnalysis(trackID, PARSERS) 512 print >> sys.stderr, "Analysis found. No upload needed." 513 except util.EchoNestAPIThingIDError: 514 print >> sys.stderr, "Analysis not found. Uploading..." 515 self.analysis = AudioAnalysis(filename, PARSERS) 516 AudioData.__init__(self, filename=filename)
517 518
519 -class LocalAnalysis(object):
520 """ 521 Like `LocalAudioFile`, it conditionally uploads the file with which 522 it was initialized. Unlike `LocalAudioFile`, it is not a subclass of 523 `AudioData`, so contains no sample data. 524 """
525 - def __init__(self, filename):
526 """ 527 :param filename: path to a local MP3 file 528 """ 529 trackID = md5.new(file(filename).read()).hexdigest() 530 print >> sys.stderr, "Computed MD5 of file is " + trackID 531 try: 532 print >> sys.stderr, "Probing for existing analysis" 533 tempanalysis = AudioAnalysis(trackID, {'duration': globalParserFloat}) 534 tempanalysis.duration 535 self.analysis = AudioAnalysis(trackID, PARSERS) 536 print >> sys.stderr, "Analysis found. No upload needed." 537 except util.EchoNestAPIThingIDError: 538 print >> sys.stderr, "Analysis not found. Uploading..." 539 self.analysis = AudioAnalysis(filename, PARSERS)
540 # no AudioData.__init__() 541 542
543 -class AudioQuantum(object) :
544 """ 545 A unit of musical time, identified at minimum with a start time and 546 a duration, both in seconds. It most often corresponds with a `section`, 547 `bar`, `beat`, `tatum`, or (by inheritance) `segment` obtained from an Analyze 548 API call. 549 550 Additional properties include: 551 552 end 553 computed time offset for convenience: `start` + `duration` 554 container 555 a circular reference to the containing `AudioQuantumList`, 556 created upon creation of the `AudioQuantumList` that covers 557 the whole track 558 """
559 - def __init__(self, start=0, duration=0, kind=None, confidence=None) :
560 """ 561 Initializes an `AudioQuantum`. 562 563 :param start: offset from the start of the track, in seconds 564 :param duration: length of the `AudioQuantum` 565 :param kind: string containing what kind of rhythm unit it came from 566 :param confidence: float between zero and one 567 """ 568 self.start = start 569 self.duration = duration 570 self.kind = kind 571 self.confidence = confidence
572
573 - def get_end(self):
574 return self.start + self.duration
575 576 end = property(get_end, doc=""" 577 A computed property: the sum of `start` and `duration`. 578 """) 579
580 - def parent(self):
581 """ 582 Returns the containing `AudioQuantum` in the rhythm hierarchy: 583 a `tatum` returns a `beat`, a `beat` returns a `bar`, and a `bar` returns a 584 `section`. 585 """ 586 pars = {'tatum': 'beats', 587 'beat': 'bars', 588 'bar': 'sections'} 589 try: 590 uppers = getattr(self.container.container, pars[self.kind]) 591 return uppers.that(selection.overlap(self))[0] 592 except LookupError: 593 # Might not be in pars, might not have anything in parent. 594 return None
595
596 - def children(self):
597 """ 598 Returns an `AudioQuantumList` of the AudioQuanta that it contains, 599 one step down the hierarchy. A `beat` returns `tatums`, a `bar` returns 600 `beats`, and a `section` returns `bars`. 601 """ 602 chils = {'beat': 'tatums', 603 'bar': 'beats', 604 'section': 'bars'} 605 try: 606 downers = getattr(self.container.container, chils[self.kind]) 607 return downers.that(selection.are_contained_by(self)) 608 except LookupError: 609 return None
610
611 - def group(self):
612 """ 613 Returns the `children`\() of the `AudioQuantum`\'s `parent`\(). 614 In other words: 'siblings'. If no parent is found, then return the 615 `AudioQuantumList` for the whole track. 616 """ 617 if self.parent(): 618 return self.parent().children() 619 else: 620 return self.container
621
622 - def prev(self, step=1):
623 """ 624 Step backwards in the containing `AudioQuantumList`. 625 Returns `self` if a boundary is reached. 626 """ 627 group = self.container 628 try: 629 loc = group.index(self) 630 new = max(loc - step, 0) 631 return group[new] 632 except: 633 return self
634
635 - def next(self, step=1):
636 """ 637 Step forward in the containing `AudioQuantumList`. 638 Returns `self` if a boundary is reached. 639 """ 640 group = self.container 641 try: 642 loc = group.index(self) 643 new = min(loc + step, len(group)) 644 return group[new] 645 except: 646 return self
647
648 - def __str__(self):
649 """ 650 Lists the `AudioQuantum`.kind with start and 651 end times, in seconds, e.g.:: 652 653 "segment (20.31 - 20.42)" 654 """ 655 return "%s (%.2f - %.2f)" % (self.kind, self.start, self.end)
656
657 - def __repr__(self):
658 """ 659 A string representing a constructor, including kind, start time, 660 duration, and (if it exists) confidence, e.g.:: 661 662 "AudioQuantum(kind='tatum', start=42.198267, duration=0.1523394)" 663 """ 664 if self.confidence is not None: 665 return "AudioQuantum(kind='%s', start=%f, duration=%f, confidence=%f)" % (self.kind, self.start, self.duration, self.confidence) 666 else: 667 return "AudioQuantum(kind='%s', start=%f, duration=%f)" % (self.kind, self.start, self.duration)
668
669 - def local_context(self):
670 """ 671 Returns a tuple of (*index*, *length*) within rhythm siblings, where 672 *index* is the (zero-indexed) position within its `group`\(), and 673 *length* is the number of siblings within its `group`\(). 674 """ 675 group = self.group() 676 count = len(group) 677 try: 678 loc = group.index(self) 679 except: # seem to be some uncontained beats 680 loc = 0 681 return (loc, count,)
682
683 - def absolute_context(self):
684 """ 685 Returns a tuple of (*index*, *length*) within the containing 686 `AudioQuantumList`, where *index* is the (zero-indexed) position within 687 its container, and *length* is the number of siblings within the 688 container. 689 """ 690 group = self.container 691 count = len(group) 692 loc = group.index(self) 693 return (loc, count,)
694
695 - def context_string(self):
696 """ 697 Returns a one-indexed, human-readable version of context. 698 For example:: 699 700 "bar 4 of 142, beat 3 of 4, tatum 2 of 3" 701 """ 702 if self.parent() and self.kind != "bar": 703 return "%s, %s %i of %i" % (self.parent().context_string(), 704 self.kind, self.local_context()[0] + 1, 705 self.local_context()[1]) 706 else: 707 return "%s %i of %i" % (self.kind, self.absolute_context()[0] + 1, 708 self.absolute_context()[1])
709
710 - def __getstate__(self):
711 """ 712 Eliminates the circular reference for pickling. 713 """ 714 dictclone = self.__dict__.copy() 715 del dictclone['container'] 716 return dictclone
717 718
719 -class AudioSegment(AudioQuantum):
720 """ 721 Subclass of `AudioQuantum` for the data-rich segments returned by 722 the Analyze API. 723 """
724 - def __init__(self, start=0., duration=0., pitches=[], timbre=[], 725 loudness_begin=0., loudness_max=0., time_loudness_max=0., loudness_end=None, kind='segment'):
726 """ 727 Initializes an `AudioSegment`. 728 729 :param start: offset from start of the track, in seconds 730 :param duration: duration of the `AudioSegment`, in seconds 731 :param pitches: a twelve-element list with relative loudnesses of each 732 pitch class, from C (pitches[0]) to B (pitches[11]) 733 :param timbre: a twelve-element list with the loudness of each of a 734 principal component of time and/or frequency profile 735 :param kind: string identifying the kind of AudioQuantum: "segment" 736 :param loudness_begin: loudness in dB at the start of the segment 737 :param loudness_max: loudness in dB at the loudest moment of the 738 segment 739 :param time_loudness_max: time (in sec from start of segment) of 740 loudest moment 741 :param loudness_end: loudness at end of segment (if it is given) 742 """ 743 self.start = start 744 self.duration = duration 745 self.pitches = pitches 746 self.timbre = timbre 747 self.loudness_begin = loudness_begin 748 self.loudness_max = loudness_max 749 self.time_loudness_max = time_loudness_max 750 if loudness_end: 751 self.loudness_end = loudness_end 752 self.kind = kind 753 self.confidence = None
754 755
756 -class AudioQuantumList(list):
757 """ 758 A container that enables content-based selection and filtering. 759 A `List` that contains `AudioQuantum` objects, with additional methods 760 for manipulating them. 761 762 When an `AudioQuantumList` is created for a track via a call to the 763 Analyze API, `attach`\() is called so that its container is set to the 764 containing `AudioAnalysis`, and the container of each of the 765 `AudioQuantum` list members is set to itself. 766 767 Additional accessors now include AudioQuantum elements such as 768 `start`, `duration`, and `confidence`, which each return a List of the 769 corresponding properties in the contained AudioQuanta. A special name 770 is `kinds`, which returns a List of the `kind` of each `AudioQuantum`. 771 If `AudioQuantumList.kind` is "`segment`", then `pitches`, `timbre`, 772 `loudness_begin`, `loudness_max`, `time_loudness_max`, and `loudness_end` 773 are available. 774 """ 775 QUANTUM_ATTRIBUTES = ['start', 'duration', 'confidence'] 776 SEGMENT_ATTRIBUTES = ['pitches', 'timbre', 'loudness_begin', 'loudness_max', 777 'time_loudness_max', 'loudness_end']
778 - def __init__(self, kind = None, container = None):
779 """ 780 Initializes an `AudioQuantumList`. 781 782 :param kind: a label for the kind of `AudioQuantum` contained 783 within 784 :param container: a reference to the containing `AudioAnalysis` 785 """ 786 list.__init__(self) 787 self.kind = kind 788 self.container = container
789
790 - def that(self, filt):
791 """ 792 Method for applying a function to each of the contained 793 `AudioQuantum` objects. Returns a new `AudioQuantumList` 794 of the same `kind` containing the `AudioQuantum` objects 795 for which the input function is true. 796 797 See `echonest.selection` for example selection filters. 798 799 :param filt: a function that takes one `AudioQuantum` and returns 800 a `True` value `None` 801 802 :change: experimenting with a filter-only form 803 """ 804 out = AudioQuantumList(kind=self.kind) 805 out.extend(filter(filt, self)) 806 return out
807
808 - def ordered_by(self, function, descending=False):
809 """ 810 Returns a new `AudioQuantumList` of the same `kind` with the 811 original elements, but ordered from low to high according to 812 the input function acting as a key. 813 814 See `echonest.sorting` for example ordering functions. 815 816 :param function: a function that takes one `AudioQuantum` and returns 817 a comparison key 818 :param descending: when `True`, reverses the sort order, from 819 high to low 820 """ 821 out = AudioQuantumList(kind=self.kind) 822 out.extend(sorted(self, key=function, reverse=descending)) 823 return out
824
825 - def beget(self, source, which=None):
826 """ 827 There are two basic forms: a map-and-flatten and an converse-that. 828 829 The basic form, with one `function` argument, returns a new 830 `AudioQuantumList` so that the source function returns 831 `None`, one, or many AudioQuanta for each `AudioQuantum` contained within 832 `self`, and flattens them, in order. :: 833 834 beats.beget(the_next_ones) 835 836 A second form has the first argument `source` as an `AudioQuantumList`, and 837 a second argument, `which`, is used as a filter for the first argument, for 838 *each* of `self`. The results are collapsed and accordianned into a flat 839 list. 840 841 For example, calling:: 842 843 beats.beget(segments, which=overlap) 844 845 Gets evaluated as:: 846 847 for beat in beats: 848 return segments.that(overlap(beat)) 849 850 And all of the `AudioQuantumList`\s that return are flattened into 851 a single `AudioQuantumList`. Note that the function passed to `which` 852 must be a function that takes a single argument that returns a function 853 returning a single argument. 854 855 :param source: A function of one argument that is applied to each 856 `AudioQuantum` of `self`, or an `AudioQuantumList`, in which case 857 the second argument is required. 858 :param which: A function of one argument that acts as a `that`\() filter 859 on the first argument if it is an `AudioQuantumList`, or as a filter 860 on the output, in the case of `source` being a function. 861 """ 862 out = AudioQuantumList() 863 if isinstance(source, AudioQuantumList): 864 if not which: 865 raise TypeError("'beget' used with an AudioQuantumList requires a second argument, 'which'") 866 out.extend(chain_from_mixed([source.that(which(x)) for x in self])) 867 else: 868 out.extend(chain_from_mixed(map(source, self))) 869 if which: 870 out = out.that(which) 871 return out
872
873 - def attach(self, container):
874 """ 875 Create circular references to the containing `AudioAnalysis` and for the 876 contained `AudioQuantum` objects. 877 """ 878 self.container = container 879 for i in self: 880 i.container = self
881
882 - def __getstate__(self):
883 """ 884 Eliminates the circular reference for pickling. 885 """ 886 dictclone = self.__dict__.copy() 887 del dictclone['container'] 888 return dictclone
889
890 - def __getattribute__(self, name):
891 """ 892 In the case of `AudioQuantum` and `AudioSegment` accessors, return the 893 corresponding ones from each of the contained AudioQuanta. If the attribute 894 is `kinds`, do the same for each `kind` accessor. Otherwise, do normal 895 attribute dispatch. 896 """ 897 if name in AudioQuantumList.SEGMENT_ATTRIBUTES and self.kind == 'segment': 898 return [getattr(x, name) for x in self] 899 elif name in AudioQuantumList.QUANTUM_ATTRIBUTES: 900 return [getattr(x, name) for x in self] 901 elif name == 'kinds': 902 return [x.kind for x in self] 903 else: 904 return object.__getattribute__(self, name)
905 906
907 -def dataParser(tag, doc):
908 """ 909 Generic XML parser for `bars`, `beats`, and `tatums`. 910 """ 911 out = AudioQuantumList(tag) 912 nodes = doc.getElementsByTagName(tag) 913 for n in nodes : 914 out.append(AudioQuantum(start=float(n.firstChild.data), kind=tag, 915 confidence=float(n.getAttributeNode('confidence').value))) 916 if len(out) > 1: 917 for i in range(len(out) - 1) : 918 out[i].duration = out[i+1].start - out[i].start 919 out[-1].duration = out[-2].duration 920 #else: 921 # out[0].duration = ??? 922 return out
923 924 925
926 -def attributeParser(tag, doc) :
927 """ 928 Generic XML parser for `sections` and (optionally) `segments`. 929 """ 930 out = AudioQuantumList(tag) 931 nodes = doc.getElementsByTagName(tag) 932 for n in nodes : 933 out.append( AudioQuantum(float(n.getAttribute('start')), 934 float(n.getAttribute('duration')), 935 tag) ) 936 return out
937 938
939 -def globalParserFloat(doc) :
940 """ 941 Generic XML parser for `tempo`, `duration`, `loudness`, `end_of_fade_in`, 942 and `start_of_fade_out`. 943 """ 944 d = doc.firstChild.childNodes[4].childNodes[0] 945 if d.getAttributeNode('confidence'): 946 return float(d.childNodes[0].data), float(d.getAttributeNode('confidence').value) 947 else: 948 return float(d.childNodes[0].data)
949 950 951
952 -def globalParserInt(doc) :
953 """ 954 Generic XML parser for `key`, `mode`, and `time_signature`. 955 """ 956 d = doc.firstChild.childNodes[4].childNodes[0] 957 if d.getAttributeNode('confidence'): 958 return int(d.childNodes[0].data), float(d.getAttributeNode('confidence').value) 959 else: 960 return int(d.childNodes[0].data)
961 962 963
964 -def barsParser(doc) :
965 return dataParser('bar', doc)
966 967 968
969 -def beatsParser(doc) :
970 return dataParser('beat', doc)
971 972 973
974 -def tatumsParser(doc) :
975 return dataParser('tatum', doc)
976 977 978
979 -def sectionsParser(doc) :
980 return attributeParser('section', doc)
981 982 983
984 -def segmentsParser(doc) :
985 return attributeParser('segment', doc)
986 987 988
989 -def metadataParser(doc) :
990 """ 991 Creates a dictionary of metadata values from the Analyze API 992 call. 993 """ 994 out = {} 995 for node in doc.firstChild.childNodes[4].childNodes: 996 out[node.nodeName] = node.firstChild.data 997 return out
998 999 1000
1001 -def fullSegmentsParser(doc):
1002 """ 1003 Full-featured parser for the XML returned by `get_segment` in the 1004 Analyze API. 1005 """ 1006 out = AudioQuantumList('segment') 1007 nodes = doc.getElementsByTagName('segment') 1008 for n in nodes: 1009 start = float(n.getAttribute('start')) 1010 duration = float(n.getAttribute('duration')) 1011 1012 loudnessnodes = n.getElementsByTagName('dB') 1013 loudness_end = None 1014 for l in loudnessnodes: 1015 if l.hasAttribute('type'): 1016 time_loudness_max = float(l.getAttribute('time')) 1017 loudness_max = float(l.firstChild.data) 1018 else: 1019 if float(l.getAttribute('time'))!=0: 1020 loudness_end = float(l.firstChild.data) 1021 else: 1022 loudness_begin = float(l.firstChild.data) 1023 1024 pitchnodes = n.getElementsByTagName('pitch') 1025 pitches=[] 1026 for p in pitchnodes: 1027 pitches.append(float(p.firstChild.data)) 1028 1029 timbrenodes = n.getElementsByTagName('coeff') 1030 timbre=[] 1031 for t in timbrenodes: 1032 timbre.append(float(t.firstChild.data)) 1033 1034 out.append(AudioSegment(start=start, duration=duration, pitches=pitches, 1035 timbre=timbre, loudness_begin=loudness_begin, 1036 loudness_max=loudness_max, time_loudness_max=time_loudness_max, loudness_end=loudness_end )) 1037 return out
1038 1039
1040 -def chain_from_mixed(iterables):
1041 """ 1042 Helper function to flatten a list of elements and lists 1043 into a list of elements. 1044 """ 1045 for y in iterables: 1046 try: 1047 iter(y) 1048 for element in y: 1049 yield element 1050 except: 1051 yield y
1052 1053 PARSERS = { 'bars' : barsParser, 1054 'beats' : beatsParser, 1055 'sections' : sectionsParser, 1056 'segments' : fullSegmentsParser, 1057 'tatums' : tatumsParser, 1058 'metadata' : metadataParser, 1059 'tempo' : globalParserFloat, 1060 'duration' : globalParserFloat, 1061 'loudness' : globalParserFloat, 1062 'end_of_fade_in' : globalParserFloat, 1063 'start_of_fade_out' : globalParserFloat, 1064 'key' : globalParserInt, 1065 'mode' : globalParserInt, 1066 'time_signature' : globalParserInt, 1067 } 1068 """ 1069 A shorthand input for `AudioAnalysis`, associating keys (which are also 1070 exposed as accessors via `AudioAnalysis.__getattribute__`\()) with 1071 parsing functions. 1072 """ 1073