/src/echonest/action.py

http://echo-nest-remix.googlecode.com/ · Python · 294 lines · 194 code · 58 blank · 42 comment · 11 complexity · 9841e1e8d943f7d212a3bce627b616b4 MD5 · raw file

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