Audio thumbnailing & spectrograms, media plugins use sniffing
[mediagoblin.git] / mediagoblin / media_types / audio / transcoders.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 import pdb
18 import logging
19 from PIL import Image
20
21 from mediagoblin.processing import BadMediaFail
22 from mediagoblin.media_types.audio import audioprocessing
23
24
25 _log = logging.getLogger(__name__)
26
27 CPU_COUNT = 2 # Just assuming for now
28
29 # IMPORT MULTIPROCESSING
30 try:
31 import multiprocessing
32 try:
33 CPU_COUNT = multiprocessing.cpu_count()
34 except NotImplementedError:
35 _log.warning('multiprocessing.cpu_count not implemented!\n'
36 'Assuming 2 CPU cores')
37 except ImportError:
38 _log.warning('Could not import multiprocessing, assuming 2 CPU cores')
39
40 # IMPORT GOBJECT
41 try:
42 import gobject
43 gobject.threads_init()
44 except ImportError:
45 raise Exception('gobject could not be found')
46
47 # IMPORT PYGST
48 try:
49 import pygst
50
51 # We won't settle for less. For now, this is an arbitrary limit
52 # as we have not tested with > 0.10
53 pygst.require('0.10')
54
55 import gst
56
57 import gst.extend.discoverer
58 except ImportError:
59 raise Exception('gst/pygst >= 0.10 could not be imported')
60
61 import numpy
62
63 class AudioThumbnailer(object):
64 def __init__(self):
65 _log.info('Initializing {0}'.format(self.__class__.__name__))
66
67 def spectrogram(self, src, dst, **kw):
68 width = kw['width']
69 height = int(kw.get('height', float(width) * 0.3))
70 fft_size = kw.get('fft_size', 2048)
71 callback = kw.get('progress_callback')
72
73 processor = audioprocessing.AudioProcessor(
74 src,
75 fft_size,
76 numpy.hanning)
77
78 samples_per_pixel = processor.audio_file.nframes / float(width)
79
80 spectrogram = audioprocessing.SpectrogramImage(width, height, fft_size)
81
82 for x in range(width):
83 if callback and x % (width / 10) == 0:
84 callback((x * 100) / width)
85
86 seek_point = int(x * samples_per_pixel)
87
88 (spectral_centroid, db_spectrum) = processor.spectral_centroid(
89 seek_point)
90
91 spectrogram.draw_spectrum(x, db_spectrum)
92
93 if callback:
94 callback(100)
95
96 spectrogram.save(dst)
97
98 def thumbnail_spectrogram(self, src, dst, thumb_size):
99 '''
100 Takes a spectrogram and creates a thumbnail from it
101 '''
102 if not (type(thumb_size) == tuple and len(thumb_size) == 2):
103 raise Exception('size argument should be a tuple(width, height)')
104
105 im = Image.open(src)
106
107 im_w, im_h = [float(i) for i in im.size]
108 th_w, th_h = [float(i) for i in thumb_size]
109
110 wadsworth_position = im_w * 0.3
111
112 start_x = max((
113 wadsworth_position - (th_w / 2.0),
114 0.0))
115
116 stop_x = start_x + (im_h * (th_w / th_h))
117
118 th = im.crop((
119 int(start_x), 0,
120 int(stop_x), int(im_h)))
121
122 if th.size[0] > th_w or th.size[1] > th_h:
123 th.thumbnail(thumb_size, Image.ANTIALIAS)
124
125 th.save(dst)
126
127
128 class AudioTranscoder(object):
129 def __init__(self):
130 _log.info('Initializing {0}'.format(self.__class__.__name__))
131
132 # Instantiate MainLoop
133 self._loop = gobject.MainLoop()
134 self._failed = None
135
136 def discover(self, src):
137 self._src_path = src
138 _log.info('Discovering {0}'.format(src))
139 self._discovery_path = src
140
141 self._discoverer = gst.extend.discoverer.Discoverer(
142 self._discovery_path)
143 self._discoverer.connect('discovered', self.__on_discovered)
144 self._discoverer.discover()
145
146 self._loop.run() # Run MainLoop
147
148 if self._failed:
149 raise self._failed
150
151 # Once MainLoop has returned, return discovery data
152 return getattr(self, '_discovery_data', False)
153
154 def __on_discovered(self, data, is_media):
155 if not is_media:
156 self._failed = BadMediaFail()
157 _log.error('Could not discover {0}'.format(self._src_path))
158 self.halt()
159
160 _log.debug('Discovered: {0}'.format(data.__dict__))
161
162 self._discovery_data = data
163
164 # Gracefully shut down MainLoop
165 self.halt()
166
167 def transcode(self, src, dst, **kw):
168 _log.info('Transcoding {0} into {1}'.format(src, dst))
169 self._discovery_data = kw.get('data', self.discover(src))
170
171 self.__on_progress = kw.get('progress_callback')
172
173 quality = kw.get('quality', 0.3)
174
175 mux_string = kw.get(
176 'mux_string',
177 'vorbisenc quality={0} ! webmmux'.format(quality))
178
179 # Set up pipeline
180 self.pipeline = gst.parse_launch(
181 'filesrc location="{src}" ! '
182 'decodebin2 ! queue ! audiorate tolerance={tolerance} ! '
183 'audioconvert ! audio/x-raw-float,channels=2 ! '
184 '{mux_string} ! '
185 'progressreport silent=true ! '
186 'filesink location="{dst}"'.format(
187 src=src,
188 tolerance=80000000,
189 mux_string=mux_string,
190 dst=dst))
191
192 self.bus = self.pipeline.get_bus()
193 self.bus.add_signal_watch()
194 self.bus.connect('message', self.__on_bus_message)
195
196 self.pipeline.set_state(gst.STATE_PLAYING)
197
198 self._loop.run()
199
200 def __on_bus_message(self, bus, message):
201 _log.debug(message)
202
203 if (message.type == gst.MESSAGE_ELEMENT
204 and message.structure.get_name() == 'progress'):
205 data = dict(message.structure)
206
207 if self.__on_progress:
208 self.__on_progress(data)
209
210 _log.info('{0}% done...'.format(
211 data.get('percent')))
212 elif message.type == gst.MESSAGE_EOS:
213 _log.info('Done')
214 self.halt()
215
216 def halt(self):
217 if getattr(self, 'pipeline', False):
218 self.pipeline.set_state(gst.STATE_NULL)
219 del self.pipeline
220 _log.info('Quitting MainLoop gracefully...')
221 gobject.idle_add(self._loop.quit)
222
223 if __name__ == '__main__':
224 import sys
225 logging.basicConfig()
226 _log.setLevel(logging.INFO)
227
228 #transcoder = AudioTranscoder()
229 #data = transcoder.discover(sys.argv[1])
230 #res = transcoder.transcode(*sys.argv[1:3])
231
232 thumbnailer = AudioThumbnailer()
233
234 thumbnailer.spectrogram(*sys.argv[1:], width=640)
235
236 pdb.set_trace()