Fix audio thumbnailing once and for all.
[mediagoblin.git] / mediagoblin / media_types / audio / transcoders.py
CommitLineData
5a34a80d
JW
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
5a34a80d 17import logging
d0e9f843
AL
18try:
19 from PIL import Image
20except ImportError:
21 import Image
5a34a80d 22
5a34a80d
JW
23_log = logging.getLogger(__name__)
24
c56d4b55 25CPU_COUNT = 2 # Just assuming for now
5a34a80d
JW
26
27# IMPORT MULTIPROCESSING
28try:
29 import multiprocessing
30 try:
31 CPU_COUNT = multiprocessing.cpu_count()
32 except NotImplementedError:
33 _log.warning('multiprocessing.cpu_count not implemented!\n'
34 'Assuming 2 CPU cores')
35except ImportError:
36 _log.warning('Could not import multiprocessing, assuming 2 CPU cores')
37
57d8212a
BB
38# uncomment this to get a lot of logs from gst
39# import os;os.environ['GST_DEBUG'] = '5,python:5'
5a34a80d 40
57d8212a
BB
41import gi
42gi.require_version('Gst', '1.0')
43from gi.repository import GObject, Gst
44Gst.init(None)
5a34a80d 45
10085b77 46import numpy
1038aea8 47import six
10085b77 48
c56d4b55 49
1038aea8 50class Python2AudioThumbnailer(object):
10085b77
JW
51 def __init__(self):
52 _log.info('Initializing {0}'.format(self.__class__.__name__))
53
54 def spectrogram(self, src, dst, **kw):
1038aea8
BS
55 # This third-party bundled module is Python 2-only.
56 from mediagoblin.media_types.audio import audioprocessing
57
10085b77
JW
58 width = kw['width']
59 height = int(kw.get('height', float(width) * 0.3))
60 fft_size = kw.get('fft_size', 2048)
61 callback = kw.get('progress_callback')
10085b77
JW
62 processor = audioprocessing.AudioProcessor(
63 src,
64 fft_size,
65 numpy.hanning)
66
67 samples_per_pixel = processor.audio_file.nframes / float(width)
68
69 spectrogram = audioprocessing.SpectrogramImage(width, height, fft_size)
70
71 for x in range(width):
72 if callback and x % (width / 10) == 0:
73 callback((x * 100) / width)
74
75 seek_point = int(x * samples_per_pixel)
76
77 (spectral_centroid, db_spectrum) = processor.spectral_centroid(
78 seek_point)
79
80 spectrogram.draw_spectrum(x, db_spectrum)
81
82 if callback:
83 callback(100)
84
85 spectrogram.save(dst)
86
87 def thumbnail_spectrogram(self, src, dst, thumb_size):
88 '''
89 Takes a spectrogram and creates a thumbnail from it
90 '''
91 if not (type(thumb_size) == tuple and len(thumb_size) == 2):
4f4f2531 92 raise Exception('thumb_size argument should be a tuple(width, height)')
10085b77
JW
93
94 im = Image.open(src)
95
96 im_w, im_h = [float(i) for i in im.size]
97 th_w, th_h = [float(i) for i in thumb_size]
98
99 wadsworth_position = im_w * 0.3
100
101 start_x = max((
4f4f2531 102 wadsworth_position - ((im_h * (th_w / th_h)) / 2.0),
10085b77
JW
103 0.0))
104
105 stop_x = start_x + (im_h * (th_w / th_h))
106
107 th = im.crop((
108 int(start_x), 0,
109 int(stop_x), int(im_h)))
110
100a73a2 111 th.thumbnail(thumb_size, Image.ANTIALIAS)
10085b77
JW
112
113 th.save(dst)
114
115
1038aea8
BS
116class Python3AudioThumbnailer(Python2AudioThumbnailer):
117 """Dummy thumbnailer for Python 3.
118
119 The Python package used for audio spectrograms, "scikits.audiolab", does not
120 support Python 3 and is a constant source of problems for people installing
121 MediaGoblin. Until the feature is rewritten, this thumbnailer class simply
122 provides a generic image.
123
124 """
125 def spectrogram(self, src, dst, **kw):
126 # Using PIL here in case someone wants to swap out the image for a PNG.
127 # This will convert to JPEG, where simply copying the file won't.
128 img = Image.open('mediagoblin/static/images/media_thumbs/video.jpg')
129 img.save(dst)
130
131
132AudioThumbnailer = Python3AudioThumbnailer if six.PY3 else Python2AudioThumbnailer
133
134
5a34a80d
JW
135class AudioTranscoder(object):
136 def __init__(self):
137 _log.info('Initializing {0}'.format(self.__class__.__name__))
138
139 # Instantiate MainLoop
57d8212a 140 self._loop = GObject.MainLoop()
ec4261a4 141 self._failed = None
5a34a80d 142
57d8212a
BB
143 def transcode(self, src, dst, mux_name='webmmux',quality=0.3,
144 progress_callback=None, **kw):
145 def _on_pad_added(element, pad, connect_to):
146 caps = pad.query_caps(None)
147 name = caps.to_string()
148 _log.debug('on_pad_added: {0}'.format(name))
149 if name.startswith('audio') and not connect_to.is_linked():
150 pad.link(connect_to)
ec4261a4 151 _log.info('Transcoding {0} into {1}'.format(src, dst))
57d8212a 152 self.__on_progress = progress_callback
5a34a80d 153 # Set up pipeline
57d8212a
BB
154 tolerance = 80000000
155 self.pipeline = Gst.Pipeline()
156 filesrc = Gst.ElementFactory.make('filesrc', 'filesrc')
157 filesrc.set_property('location', src)
158 decodebin = Gst.ElementFactory.make('decodebin', 'decodebin')
159 queue = Gst.ElementFactory.make('queue', 'queue')
160 decodebin.connect('pad-added', _on_pad_added,
161 queue.get_static_pad('sink'))
162 audiorate = Gst.ElementFactory.make('audiorate', 'audiorate')
163 audiorate.set_property('tolerance', tolerance)
164 audioconvert = Gst.ElementFactory.make('audioconvert', 'audioconvert')
165 caps_struct = Gst.Structure.new_empty('audio/x-raw')
166 caps_struct.set_value('channels', 2)
167 caps = Gst.Caps.new_empty()
168 caps.append_structure(caps_struct)
169 capsfilter = Gst.ElementFactory.make('capsfilter', 'capsfilter')
170 capsfilter.set_property('caps', caps)
171 enc = Gst.ElementFactory.make('vorbisenc', 'enc')
172 enc.set_property('quality', quality)
173 mux = Gst.ElementFactory.make(mux_name, 'mux')
174 progressreport = Gst.ElementFactory.make('progressreport', 'progress')
175 progressreport.set_property('silent', True)
176 sink = Gst.ElementFactory.make('filesink', 'sink')
177 sink.set_property('location', dst)
178 # add to pipeline
179 for e in [filesrc, decodebin, queue, audiorate, audioconvert,
180 capsfilter, enc, mux, progressreport, sink]:
181 self.pipeline.add(e)
182 # link elements
183 filesrc.link(decodebin)
184 decodebin.link(queue)
185 queue.link(audiorate)
186 audiorate.link(audioconvert)
187 audioconvert.link(capsfilter)
188 capsfilter.link(enc)
189 enc.link(mux)
190 mux.link(progressreport)
191 progressreport.link(sink)
5a34a80d
JW
192 self.bus = self.pipeline.get_bus()
193 self.bus.add_signal_watch()
194 self.bus.connect('message', self.__on_bus_message)
57d8212a
BB
195 # run
196 self.pipeline.set_state(Gst.State.PLAYING)
5a34a80d
JW
197 self._loop.run()
198
199 def __on_bus_message(self, bus, message):
57d8212a
BB
200 _log.debug(message.type)
201 if (message.type == Gst.MessageType.ELEMENT
202 and message.has_name('progress')):
203 structure = message.get_structure()
204 (success, percent) = structure.get_int('percent')
205 if self.__on_progress and success:
206 self.__on_progress(percent)
207 _log.info('{0}% done...'.format(percent))
208 elif message.type == Gst.MessageType.EOS:
5a34a80d
JW
209 _log.info('Done')
210 self.halt()
57d8212a
BB
211 elif message.type == Gst.MessageType.ERROR:
212 _log.error(message.parse_error())
213 self.halt()
5a34a80d
JW
214
215 def halt(self):
10085b77 216 if getattr(self, 'pipeline', False):
57d8212a 217 self.pipeline.set_state(Gst.State.NULL)
10085b77 218 del self.pipeline
5a34a80d 219 _log.info('Quitting MainLoop gracefully...')
57d8212a 220 GObject.idle_add(self._loop.quit)
5a34a80d
JW
221
222if __name__ == '__main__':
223 import sys
224 logging.basicConfig()
225 _log.setLevel(logging.INFO)
226
10085b77
JW
227 #transcoder = AudioTranscoder()
228 #data = transcoder.discover(sys.argv[1])
229 #res = transcoder.transcode(*sys.argv[1:3])
230
231 thumbnailer = AudioThumbnailer()
232
233 thumbnailer.spectrogram(*sys.argv[1:], width=640)