Commit | Line | Data |
---|---|---|
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 | 17 | import logging |
d0e9f843 AL |
18 | try: |
19 | from PIL import Image | |
20 | except ImportError: | |
21 | import Image | |
5a34a80d | 22 | |
5a34a80d JW |
23 | _log = logging.getLogger(__name__) |
24 | ||
c56d4b55 | 25 | CPU_COUNT = 2 # Just assuming for now |
5a34a80d JW |
26 | |
27 | # IMPORT MULTIPROCESSING | |
28 | try: | |
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') | |
35 | except 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 |
41 | import gi |
42 | gi.require_version('Gst', '1.0') | |
43 | from gi.repository import GObject, Gst | |
44 | Gst.init(None) | |
5a34a80d | 45 | |
10085b77 | 46 | import numpy |
1038aea8 | 47 | import six |
10085b77 | 48 | |
c56d4b55 | 49 | |
1038aea8 | 50 | class 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 |
116 | class 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 | ||
132 | AudioThumbnailer = Python3AudioThumbnailer if six.PY3 else Python2AudioThumbnailer | |
133 | ||
134 | ||
5a34a80d JW |
135 | class 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 | |
222 | if __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) |