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
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
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
73
74
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
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
120 doc = analyze.upload(audio)
121 self.id = doc.getElementsByTagName('thingID')[0].firstChild.data
122 else:
123
124 self.id = audio
125
126
127 for cachedVar in AudioAnalysis.CACHED_VARIABLES :
128 self.__setattr__(cachedVar, None)
129
137
155
164
165
166
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
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
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
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
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
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
281 return AudioData(None, self.data[index])
282
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
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
300 if self.data is not None:
301 return len(self.data)
302 else:
303 return 0
304
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
317
318
319 print >> sys.stderr, "Unknown file extension, saving as mp3"
320 self.encode_mp3(filename + ".mp3")
321
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
332
333 num_channels = 2
334
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
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
352 frames = data_stereo[start:start+num_samples_per_enc_run].tostring()
353 else:
354
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
366 "Help `encode`\() write out a WAVE file."
367
368 if filename is None:
369 foo,filename = tempfile.mkstemp(".wav")
370
371
372 fid = open(filename, 'wb')
373 fid.write('RIFF')
374 fid.write('\x00\x00\x00\x00')
375 fid.write('WAVE')
376
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
387 fid.write('data')
388 fid.write(struct.pack('l', self.data.nbytes))
389 self.data.tofile(fid)
390
391
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
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
414 dur = 0
415 for s in segs:
416 dur += int(s.duration*audioData.sampleRate)
417
418 dur += 100000
419
420
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
429 newAD = AudioData(shape=newshape,sampleRate=audioData.sampleRate, numChannels=newchans)
430
431
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
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 """
470
471
472
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
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 """
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
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 """
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
541
542
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
575
576 end = property(get_end, doc="""
577 A computed property: the sum of `start` and `duration`.
578 """)
579
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
594 return None
595
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
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
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
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:
680 loc = 0
681 return (loc, count,)
682
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
711 """
712 Eliminates the circular reference for pickling.
713 """
714 dictclone = self.__dict__.copy()
715 del dictclone['container']
716 return dictclone
717
718
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
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
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
883 """
884 Eliminates the circular reference for pickling.
885 """
886 dictclone = self.__dict__.copy()
887 del dictclone['container']
888 return dictclone
889
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
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
921
922 return out
923
924
925
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
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
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
966
967
968
971
972
973
976
977
978
981
982
983
986
987
988
998
999
1000
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
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