Port of audio to GStreamer 1.0
[larjonas-mediagoblin.git] / mediagoblin / media_types / audio / processing.py
blob770342ffcd041c9f9682292386c408659164858f
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
17 import argparse
18 import logging
19 import os
21 from mediagoblin import mg_globals as mgg
22 from mediagoblin.processing import (
23 BadMediaFail, FilenameBuilder,
24 ProgressCallback, MediaProcessor, ProcessingManager,
25 request_from_args, get_process_filename,
26 store_public, copy_original)
28 from mediagoblin.media_types.audio.transcoders import (
29 AudioTranscoder, AudioThumbnailer)
30 from mediagoblin.media_types.tools import discover
32 _log = logging.getLogger(__name__)
34 MEDIA_TYPE = 'mediagoblin.media_types.audio'
37 def sniff_handler(media_file, filename):
38 _log.info('Sniffing {0}'.format(MEDIA_TYPE))
39 data = discover(media_file.name)
40 if data and data.get_audio_streams() and not data.get_video_streams():
41 return MEDIA_TYPE
42 return None
45 class CommonAudioProcessor(MediaProcessor):
46 """
47 Provides a base for various audio processing steps
48 """
49 acceptable_files = ['original', 'best_quality', 'webm_audio']
51 def common_setup(self):
52 """
53 Setup the workbench directory and pull down the original file, add
54 the audio_config, transcoder, thumbnailer and spectrogram_tmp path
55 """
56 self.audio_config = mgg \
57 .global_config['plugins']['mediagoblin.media_types.audio']
59 # Pull down and set up the processing file
60 self.process_filename = get_process_filename(
61 self.entry, self.workbench, self.acceptable_files)
62 self.name_builder = FilenameBuilder(self.process_filename)
64 self.transcoder = AudioTranscoder()
65 self.thumbnailer = AudioThumbnailer()
67 def copy_original(self):
68 if self.audio_config['keep_original']:
69 copy_original(
70 self.entry, self.process_filename,
71 self.name_builder.fill('{basename}{ext}'))
73 def _keep_best(self):
74 """
75 If there is no original, keep the best file that we have
76 """
77 if not self.entry.media_files.get('best_quality'):
78 # Save the best quality file if no original?
79 if not self.entry.media_files.get('original') and \
80 self.entry.media_files.get('webm_audio'):
81 self.entry.media_files['best_quality'] = self.entry \
82 .media_files['webm_audio']
84 def _skip_processing(self, keyname, **kwargs):
85 file_metadata = self.entry.get_file_metadata(keyname)
86 skip = True
88 if not file_metadata:
89 return False
91 if keyname == 'webm_audio':
92 if kwargs.get('quality') != file_metadata.get('quality'):
93 skip = False
94 elif keyname == 'spectrogram':
95 if kwargs.get('max_width') != file_metadata.get('max_width'):
96 skip = False
97 elif kwargs.get('fft_size') != file_metadata.get('fft_size'):
98 skip = False
99 elif keyname == 'thumb':
100 if kwargs.get('size') != file_metadata.get('size'):
101 skip = False
103 return skip
105 def transcode(self, quality=None):
106 if not quality:
107 quality = self.audio_config['quality']
109 if self._skip_processing('webm_audio', quality=quality):
110 return
112 progress_callback = ProgressCallback(self.entry)
113 webm_audio_tmp = os.path.join(self.workbench.dir,
114 self.name_builder.fill(
115 '{basename}{ext}'))
117 self.transcoder.transcode(
118 self.process_filename,
119 webm_audio_tmp,
120 quality=quality,
121 progress_callback=progress_callback)
123 self._keep_best()
125 _log.debug('Saving medium...')
126 store_public(self.entry, 'webm_audio', webm_audio_tmp,
127 self.name_builder.fill('{basename}.medium.webm'))
129 self.entry.set_file_metadata('webm_audio', **{'quality': quality})
131 def create_spectrogram(self, max_width=None, fft_size=None):
132 if not max_width:
133 max_width = mgg.global_config['media:medium']['max_width']
134 if not fft_size:
135 fft_size = self.audio_config['spectrogram_fft_size']
137 if self._skip_processing('spectrogram', max_width=max_width,
138 fft_size=fft_size):
139 return
140 wav_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
141 '{basename}.ogg'))
142 _log.info('Creating OGG source for spectrogram')
143 self.transcoder.transcode(self.process_filename, wav_tmp,
144 mux_name='oggmux')
145 spectrogram_tmp = os.path.join(self.workbench.dir,
146 self.name_builder.fill(
147 '{basename}-spectrogram.jpg'))
148 self.thumbnailer.spectrogram(
149 wav_tmp,
150 spectrogram_tmp,
151 width=max_width,
152 fft_size=fft_size)
154 _log.debug('Saving spectrogram...')
155 store_public(self.entry, 'spectrogram', spectrogram_tmp,
156 self.name_builder.fill('{basename}.spectrogram.jpg'))
158 file_metadata = {'max_width': max_width,
159 'fft_size': fft_size}
160 self.entry.set_file_metadata('spectrogram', **file_metadata)
162 def generate_thumb(self, size=None):
163 if not size:
164 max_width = mgg.global_config['media:thumb']['max_width']
165 max_height = mgg.global_config['media:thumb']['max_height']
166 size = (max_width, max_height)
168 if self._skip_processing('thumb', size=size):
169 return
171 thumb_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
172 '{basename}-thumbnail.jpg'))
174 # We need the spectrogram to create a thumbnail
175 spectrogram = self.entry.media_files.get('spectrogram')
176 if not spectrogram:
177 _log.info('No spectrogram found, we will create one.')
178 self.create_spectrogram()
179 spectrogram = self.entry.media_files['spectrogram']
181 spectrogram_filepath = mgg.public_store.get_local_path(spectrogram)
183 self.thumbnailer.thumbnail_spectrogram(
184 spectrogram_filepath,
185 thumb_tmp,
186 tuple(size))
188 store_public(self.entry, 'thumb', thumb_tmp,
189 self.name_builder.fill('{basename}.thumbnail.jpg'))
191 self.entry.set_file_metadata('thumb', **{'size': size})
194 class InitialProcessor(CommonAudioProcessor):
196 Initial processing steps for new audio
198 name = "initial"
199 description = "Initial processing"
201 @classmethod
202 def media_is_eligible(cls, entry=None, state=None):
204 Determine if this media type is eligible for processing
206 if not state:
207 state = entry.state
208 return state in (
209 "unprocessed", "failed")
211 @classmethod
212 def generate_parser(cls):
213 parser = argparse.ArgumentParser(
214 description=cls.description,
215 prog=cls.name)
217 parser.add_argument(
218 '--quality',
219 type=float,
220 help='vorbisenc quality. Range: -0.1..1')
222 parser.add_argument(
223 '--fft_size',
224 type=int,
225 help='spectrogram fft size')
227 parser.add_argument(
228 '--thumb_size',
229 nargs=2,
230 metavar=('max_width', 'max_height'),
231 type=int,
232 help='minimum size is 100 x 100')
234 parser.add_argument(
235 '--medium_width',
236 type=int,
237 help='The width of the spectogram')
239 return parser
241 @classmethod
242 def args_to_request(cls, args):
243 return request_from_args(
244 args, ['quality', 'fft_size',
245 'thumb_size', 'medium_width'])
247 def process(self, quality=None, fft_size=None, thumb_size=None,
248 medium_width=None):
249 self.common_setup()
251 self.transcode(quality=quality)
252 self.copy_original()
254 self.create_spectrogram(max_width=medium_width, fft_size=fft_size)
255 self.generate_thumb(size=thumb_size)
257 self.delete_queue_file()
260 class Resizer(CommonAudioProcessor):
262 Thumbnail and spectogram resizing process steps for processed audio
264 name = 'resize'
265 description = 'Resize thumbnail or spectogram'
266 thumb_size = 'thumb_size'
268 @classmethod
269 def media_is_eligible(cls, entry=None, state=None):
271 Determine if this media entry is eligible for processing
273 if not state:
274 state = entry.state
275 return state in 'processed'
277 @classmethod
278 def generate_parser(cls):
279 parser = argparse.ArgumentParser(
280 description=cls.description,
281 prog=cls.name)
283 parser.add_argument(
284 '--fft_size',
285 type=int,
286 help='spectrogram fft size')
288 parser.add_argument(
289 '--thumb_size',
290 nargs=2,
291 metavar=('max_width', 'max_height'),
292 type=int,
293 help='minimum size is 100 x 100')
295 parser.add_argument(
296 '--medium_width',
297 type=int,
298 help='The width of the spectogram')
300 parser.add_argument(
301 'file',
302 choices=['thumb', 'spectrogram'])
304 return parser
306 @classmethod
307 def args_to_request(cls, args):
308 return request_from_args(
309 args, ['thumb_size', 'file', 'fft_size', 'medium_width'])
311 def process(self, file, thumb_size=None, fft_size=None,
312 medium_width=None):
313 self.common_setup()
315 if file == 'thumb':
316 self.generate_thumb(size=thumb_size)
317 elif file == 'spectrogram':
318 self.create_spectrogram(max_width=medium_width, fft_size=fft_size)
321 class Transcoder(CommonAudioProcessor):
323 Transcoding processing steps for processed audio
325 name = 'transcode'
326 description = 'Re-transcode audio'
328 @classmethod
329 def media_is_eligible(cls, entry=None, state=None):
330 if not state:
331 state = entry.state
332 return state in 'processed'
334 @classmethod
335 def generate_parser(cls):
336 parser = argparse.ArgumentParser(
337 description=cls.description,
338 prog=cls.name)
340 parser.add_argument(
341 '--quality',
342 help='vorbisenc quality. Range: -0.1..1')
344 return parser
346 @classmethod
347 def args_to_request(cls, args):
348 return request_from_args(
349 args, ['quality'])
351 def process(self, quality=None):
352 self.common_setup()
353 self.transcode(quality=quality)
356 class AudioProcessingManager(ProcessingManager):
357 def __init__(self):
358 super(AudioProcessingManager, self).__init__()
359 self.add_processor(InitialProcessor)
360 self.add_processor(Resizer)
361 self.add_processor(Transcoder)