PageRenderTime 322ms CodeModel.GetById 120ms app.highlight 65ms RepoModel.GetById 77ms app.codeStats 0ms

/src/echonest/action.py

http://echo-nest-remix.googlecode.com/
Python | 294 lines | 194 code | 58 blank | 42 comment | 17 complexity | 9841e1e8d943f7d212a3bce627b616b4 MD5 | raw file
  1#!/usr/bin/env python
  2# encoding: utf-8
  3"""
  4action.py
  5
  6Created by Tristan Jehan and Jason Sundram.
  7"""
  8import os
  9from numpy import zeros, multiply, float32, mean, copy
 10from math import atan, pi
 11import sys
 12
 13from echonest.audio import assemble, AudioData
 14from cAction import limit, crossfade, fadein, fadeout
 15
 16import dirac
 17
 18def rows(m):
 19    """returns the # of rows in a numpy matrix"""
 20    return m.shape[0]
 21
 22def make_mono(track):
 23    """Converts stereo tracks to mono; leaves mono tracks alone."""
 24    if track.data.ndim == 2:
 25        mono = mean(track.data,1)
 26        track.data = mono
 27        track.numChannels = 1
 28    return track
 29
 30def make_stereo(track):
 31    """If the track is mono, doubles it. otherwise, does nothing."""
 32    if track.data.ndim == 1:
 33        stereo = zeros((len(track.data), 2))
 34        stereo[:,0] = copy(track.data)
 35        stereo[:,1] = copy(track.data)
 36        track.data = stereo
 37        track.numChannels = 2
 38    return track
 39    
 40def render(actions, filename, verbose=True):
 41    """Calls render on each action in actions, concatenates the results, 
 42    renders an audio file, and returns a path to the file"""
 43    pieces = [a.render() for a in actions]
 44    # TODO: allow numChannels and sampleRate to vary.
 45    out = assemble(pieces, numChannels=2, sampleRate=44100, verbose=verbose)
 46    return out, out.encode(filename)
 47
 48
 49class Playback(object):
 50    """A snippet of the given track with start and duration. Volume leveling 
 51    may be applied."""
 52    def __init__(self, track, start, duration):
 53        self.track = track
 54        self.start = float(start)
 55        self.duration = float(duration)
 56    
 57    def render(self):
 58        # self has start and duration, so it is a valid index into track.
 59        output = self.track[self]
 60        # Normalize volume if necessary
 61        gain = getattr(self.track, 'gain', None)
 62        if gain != None:
 63            # limit expects a float32 vector
 64            output.data = limit(multiply(output.data, float32(gain)))
 65            
 66        return output
 67    
 68    def __repr__(self):
 69        return "<Playback '%s'>" % self.track.analysis.pyechonest_track.title
 70    
 71    def __str__(self):
 72        args = (self.start, self.start + self.duration, 
 73                self.duration, self.track.analysis.pyechonest_track.title)
 74        return "Playback\t%.3f\t-> %.3f\t (%.3f)\t%s" % args
 75
 76
 77class Fadeout(Playback):
 78    """Fadeout"""
 79    def render(self):
 80        gain = getattr(self.track, 'gain', 1.0)
 81        output = self.track[self]
 82        # second parameter is optional -- in place function for now
 83        output.data = fadeout(output.data, gain)
 84        return output
 85    
 86    def __repr__(self):
 87        return "<Fadeout '%s'>" % self.track.analysis.pyechonest_track.title
 88    
 89    def __str__(self):
 90        args = (self.start, self.start + self.duration, 
 91                self.duration, self.track.analysis.pyechonest_track.title)
 92        return "Fade out\t%.3f\t-> %.3f\t (%.3f)\t%s" % args
 93
 94
 95class Fadein(Playback):
 96    """Fadein"""
 97    def render(self):
 98        gain = getattr(self.track, 'gain', 1.0)
 99        output = self.track[self]
100        # second parameter is optional -- in place function for now
101        output.data = fadein(output.data, gain)
102        return output
103    
104    def __repr__(self):
105        return "<Fadein '%s'>" % self.track.analysis.pyechonest_track.title
106    
107    def __str__(self):
108        args = (self.start, self.start + self.duration, 
109                self.duration, self.track.analysis.pyechonest_track.title)
110        return "Fade in\t%.3f\t-> %.3f\t (%.3f)\t%s" % args
111
112
113class Edit(object):
114    """Refer to a snippet of audio"""
115    def __init__(self, track, start, duration):
116        self.track = track
117        self.start = float(start)
118        self.duration = float(duration)
119    
120    def __str__(self):
121        args = (self.start, self.start + self.duration, 
122                self.duration, self.track.analysis.pyechonest_track.title)
123        return "Edit\t%.3f\t-> %.3f\t (%.3f)\t%s" % args
124    
125    def get(self):
126        return self.track[self]
127    
128    @property
129    def end(self):
130        return self.start + self.duration
131
132
133class Crossfade(object):
134    """Crossfades between two tracks, at the start points specified, 
135    for the given duration"""
136    def __init__(self, tracks, starts, duration, mode='linear'):
137        self.t1, self.t2 = [Edit(t, s, duration) for t,s in zip(tracks, starts)]
138        self.duration = self.t1.duration
139        self.mode = mode
140    
141    def render(self):
142        t1, t2 = map(make_stereo, (self.t1.get(), self.t2.get()))
143        vecout = crossfade(t1.data, t2.data, self.mode)
144        audio_out = AudioData(ndarray=vecout, shape=vecout.shape, 
145                                sampleRate=t1.sampleRate, 
146                                numChannels=vecout.shape[1])
147        return audio_out
148    
149    def __repr__(self):
150        args = (analysis.pyechonest_track.title, self.t2.track.analysis.pyechonest_track.title)
151        return "<Crossfade '%s' and '%s'>" % args
152    
153    def __str__(self):
154        args = (self.t1.start, self.t2.start + self.duration, self.duration, 
155                self.t1.track.analysis.pyechonest_track.title, self.t2.track.analysis.pyechonest_track.title)
156        return "Crossfade\t%.3f\t-> %.3f\t (%.3f)\t%s -> %s" % args
157
158
159class Jump(Crossfade):
160    """Move from one point """
161    def __init__(self, track, source, target, duration):
162        self.track = track
163        self.t1, self.t2 = (Edit(track, source, duration), 
164                            Edit(track, target - duration, duration))
165        self.duration = float(duration)
166        self.mode = 'equal_power'
167        self.CROSSFADE_COEFF = 0.6
168    
169    @property
170    def source(self):
171        return self.t1.start
172    
173    @property
174    def target(self):
175        return self.t2.end 
176    
177    def __repr__(self):
178        return "<Jump '%s'>" % (self.t1.track.analysis.pyechonest_track.title)
179    
180    def __str__(self):
181        args = (self.t1.start, self.t2.end, self.duration, 
182                self.t1.track.analysis.pyechonest_track.title)
183        return "Jump\t\t%.3f\t-> %.3f\t (%.3f)\t%s" % args
184
185
186class Blend(object):
187    """Mix together two lists of beats"""
188    def __init__(self, tracks, lists):
189        self.t1, self.t2 = tracks
190        self.l1, self.l2 = lists
191        assert(len(self.l1) == len(self.l2))
192        
193        self.calculate_durations()
194    
195    def calculate_durations(self):
196        zipped = zip(self.l1, self.l2)
197        self.durations = [(d1 + d2) / 2.0 for ((s1, d1), (s2, d2)) in zipped]
198        self.duration = sum(self.durations)
199    
200    def render(self):
201        # use self.durations already computed
202        # build 2 AudioQuantums
203        # call Mix
204        pass
205    
206    def __repr__(self):
207        args = (self.t1.analysis.pyechonest_track.title, self.t2.analysis.pyechonest_track.title)
208        return "<Blend '%s' and '%s'>" % args
209    
210    def __str__(self):
211        # start and end for each of these lists.
212        s1, e1 = self.l1[0][0], sum(self.l1[-1])
213        s2, e2 = self.l2[0][0], sum(self.l2[-1])
214        n1, n2 = self.t1.analysis.pyechonest_track.title, self.t2.analysis.pyechonest_track.title # names
215        args = (s1, s2, e1, e2, self.duration, n1, n2)
216        return "Blend [%.3f, %.3f] -> [%.3f, %.3f] (%.3f)\t%s + %s" % args
217
218
219class Crossmatch(Blend):
220    """Makes a beat-matched crossfade between the two input tracks."""
221    def calculate_durations(self):
222        c, dec = 1.0, 1.0 / float(len(self.l1)+1)
223        self.durations = []
224        for ((s1, d1), (s2, d2)) in zip(self.l1, self.l2):
225            c -= dec
226            self.durations.append(c * d1 + (1 - c) * d2)
227        self.duration = sum(self.durations)
228    
229    def stretch(self, t, l):
230        """t is a track, l is a list"""
231        signal_start = int(l[0][0] * t.sampleRate)
232        signal_duration = int((sum(l[-1]) - l[0][0]) * t.sampleRate)
233        vecin = t.data[signal_start:signal_start + signal_duration,:]
234        
235        rates = []
236        for i in xrange(len(l)):
237            rate = (int(l[i][0] * t.sampleRate) - signal_start, 
238                    self.durations[i] / l[i][1])
239            rates.append(rate)
240        
241        vecout = dirac.timeScale(vecin, rates, t.sampleRate, 0)
242        if hasattr(t, 'gain'):
243            vecout = limit(multiply(vecout, float32(t.gain)))
244        
245        audio_out = AudioData(ndarray=vecout, shape=vecout.shape, 
246                                sampleRate=t.sampleRate, 
247                                numChannels=vecout.shape[1])
248        return audio_out
249    
250    def render(self):
251        # use self.durations already computed
252        # 1) stretch the duration of each item in t1 and t2
253        # to the duration prescribed in durations.
254        out1 = self.stretch(self.t1, self.l1)
255        out2 = self.stretch(self.t2, self.l2)
256        
257        # 2) cross-fade the results
258        # out1.duration, out2.duration, and self.duration should be about 
259        # the same, but it never hurts to be safe.
260        duration = min(out1.duration, out2.duration, self.duration)
261        c = Crossfade([out1, out2], [0, 0], duration, mode='equal_power')
262        return c.render()
263    
264    def __repr__(self):
265        args = (self.t1.analysis.pyechonest_track.title, self.t2.analysis.pyechonest_track.title)
266        return "<Crossmatch '%s' and '%s'>" % args
267    
268    def __str__(self):
269        # start and end for each of these lists.
270        s1, e1 = self.l1[0][0], sum(self.l1[-1])
271        s2, e2 = self.l2[0][0], sum(self.l2[-1])
272        n1, n2 = self.t1.analysis.pyechonest_track.title, self.t2.analysis.pyechonest_track.title # names
273        args = (s1, e2, self.duration, n1, n2)
274        return "Crossmatch\t%.3f\t-> %.3f\t (%.3f)\t%s -> %s" % args
275
276
277def humanize_time(secs):
278    """Turns seconds into a string of the form HH:MM:SS, 
279    or MM:SS if less than one hour."""
280    mins, secs = divmod(secs, 60)
281    hours, mins = divmod(mins, 60)
282    if 0 < hours: 
283        return '%02d:%02d:%02d' % (hours, mins, secs)
284    
285    return '%02d:%02d' % (mins, secs)
286
287
288def display_actions(actions):
289    total = 0
290    print
291    for a in actions:
292        print "%s\t  %s" % (humanize_time(total), unicode(a))
293        total += a.duration
294    print