initial replaygain support, only for oggvorbis
[audiomangler.git] / audiomangler / codecs.py
blob3fdf2b47fc05c72f75923b00db8923e93b77e413
1 ###########################################################################
2 # Copyright (C) 2008 by Andrew Mahone
3 # <andrew.mahone@gmail.com>
5 # Copyright: See COPYING file that comes with this distribution
7 ###########################################################################
8 import os, os.path
9 import sys
10 import Image
11 try:
12 from cStringIO import StringIO
13 except ImportError:
14 from StringIO import StringIO
15 from tempfile import mkdtemp
16 from threading import BoundedSemaphore, RLock
17 from mutagen import FileType
18 from audiomangler import Config, from_config, FuncTask, CLITask, TaskSet, Expr, File, util
20 class CodecMeta(type):
21 def __new__(cls, name, bases, cls_dict):
22 class_init = cls_dict.get('__classinit__',None)
23 if class_init:
24 cls_dict['__classinit__'] = staticmethod(class_init)
25 return super(CodecMeta,cls).__new__(cls, name, bases, cls_dict)
27 def __init__(self, name, bases, cls_dict):
28 if callable(getattr(self,'__classinit__',None)):
29 self.__classinit__(self, name, bases, cls_dict)
31 codec_map = {}
33 class Codec(object):
34 __metaclass__ = CodecMeta
36 def __classinit__(cls, name, bases, cls_dict):
37 if 'type_' in cls_dict:
38 codec_map[cls_dict['type_']] = cls
40 @classmethod
41 def _conv_out_filename(cls, filename):
42 return ''.join((filename.rsplit('.',1)[0],'.',cls.ext))
44 @classmethod
45 def from_wav_multi(cls,indir,infiles,outfiles):
46 if not getattr(cls,'_from_wav_multi_cmd',None):
47 return None
48 encopts = Config['encopts']
49 if not encopts:
50 encopts = ()
51 else:
52 encopts = tuple(encopts.split())
53 args = cls._from_wav_multi_cmd.evaluate({
54 'indir':indir,
55 'infiles':tuple(infiles),
56 'outfiles':tuple(outfiles),
57 'ext':cls.ext,
58 'type':cls.type_,
59 'encopts':encopts,
60 'encoder':cls.encoder
62 return (CLITask(args=args,stdin='/dev/null',stdout='/dev/null',stderr=sys.stderr,background=True))
64 @classmethod
65 def from_wav_pipe(cls, infile, outfile):
66 if not getattr(cls,'_from_wav_pipe_cmd',None):
67 return None
68 encopts = Config['encopts']
69 if not encopts:
70 encopts = ()
71 else:
72 encopts = tuple(encopts.split())
73 outfile = cls._conv_out_filename(infile)
74 env = {
75 'infile': infile,
76 'outfile': outfile,
77 'ext': cls.ext,
78 'type': cls.type_,
79 'encopts': encopts,
80 'encoder': cls.encoder
82 args = cls._from_wav_pipe_cmd.evaluate(env)
83 stdin = '/dev/null'
84 if hasattr(cls,'_from_wav_pipe_stdin'):
85 stdin = cls._from_wav_pipe_stdin.evaluate(env)
86 return CLITask(args=args,stdin=stdin,stdout='/dev/null',stderr=sys.stderr,background=True)
88 @classmethod
89 def to_wav_pipe(cls, infile, outfile):
90 if not getattr(cls,'_to_wav_pipe_cmd',None):
91 return None
92 env = {
93 'infile': infile,
94 'outfile': outfile,
95 'ext':cls.ext,
96 'type':cls.type_,
97 'decoder':cls.decoder
99 args = cls._to_wav_pipe_cmd.evaluate(env)
100 stdout = '/dev/null'
101 if hasattr(cls,'_to_wav_pipe_stdout'):
102 stdout = cls._to_wav_pipe_stdout.evaluate(env)
103 return CLITask(args=args,stdin='/dev/null',stdout=stdout,stderr=sys.stderr,background=False)
105 @classmethod
106 def add_replaygain(cls,files):
107 env = {
108 'replaygain':cls.replaygain,
109 'files':tuple(files)
111 args = cls._replaygain_cmd.evaluate(env)
112 return CLITask(args=args,stdin='/dev/null',stdout='/dev/null',stderr=sys.stderr,background=False)
114 class MP3Codec(Codec):
115 ext = 'mp3'
116 type_ = 'mp3'
117 encoder = 'lame'
118 replaygain = 'mp3gain'
119 _from_wav_multi_cmd = Expr("(encoder,'--quiet')+encopts+('--noreplaygain','--nogapout',indir,'--nogaptags','--nogap')+infiles")
121 class WavPackCodec(Codec):
122 ext = 'wv'
123 type_ = 'wavpack'
124 encoder = 'wavpack'
125 decoder = 'wvunpack'
126 replaygain = 'wvgain'
127 _to_wav_pipe_cmd = Expr("(decoder,'-q','-w',infile,'-o','-')")
128 _to_wav_pipe_stdout = Expr("outfile")
129 _from_wav_pipe_cmd = Expr("(encoder,'-q')+encopts+(infile,'-o',outfile)")
131 class FLACCodec(Codec):
132 ext = 'flac'
133 type_ = 'flac'
134 encoder = 'flac'
135 decoder = 'flac'
136 replaygain = 'metaflac'
137 _to_wav_pipe_cmd = Expr("(decoder,'-s','-c','-d',infile)")
138 _to_wav_pipe_stdout = Expr("outfile")
139 _from_wav_pipe_cmd = Expr("(encoder,'-s')+encopts+(infile,)")
141 class OggVorbisCodec(Codec):
142 ext = 'ogg'
143 type_ = 'oggvorbis'
144 encoder = 'oggenc'
145 decoder = 'oggdec'
146 replaygain = 'vorbisgain'
147 _to_wav_pipe_cmd = Expr("(decoder,'-Q','-o','-',infile)")
148 _to_wav_pipe_stdout = Expr("outfile")
149 _from_wav_pipe_cmd = Expr("(encoder,'-Q')+encopts+('-o',outfile,infile)")
150 _replaygain_cmd = Expr("(replaygain,'-q','-a')+files)")
152 def transcode_track(dtask, etask, sem):
153 etask.run()
154 dtask.run()
155 etask.wait()
156 if sem:
157 sem.release()
159 def check_and_copy_cover(fileset,targetfiles):
160 cover_sizes = Config['cover_sizes']
161 if not cover_sizes:
162 return
163 cover_out_filename = Config['cover_out_filename']
164 if not cover_out_filename:
165 return
166 cover_out_filename = Expr(cover_out_filename)
167 cover_sizes = cover_sizes.split(',')
168 covers_loaded = {}
169 covers_written = {}
170 outdirs = set()
171 cover_filenames = Config['cover_filenames']
172 if cover_filenames:
173 cover_filenames = cover_filenames.split(',')
174 else:
175 cover_filenames = ()
176 for (infile,targetfile) in zip(fileset,targetfiles):
177 if infile.meta['dir'] in outdirs: continue
178 i = None
179 for filename in (os.path.join(infile.meta['dir'],f) for f in cover_filenames):
180 try:
181 d = open(filename).read()
182 i = Image.open(StringIO(d))
183 i.load()
184 except Exception:
185 continue
186 if not i:
187 tags = [(value.type,value) for key,value in infile.tags.items() if key.startswith('APIC') and \
188 hasattr(value,'type') and value.type in (0,3)]
189 tags.sort(None,None,True)
190 for t,value in tags:
191 i = None
192 try:
193 d = value.data
194 i = Image.open(StringIO(d))
195 i.load()
196 break
197 except Exception:
198 continue
199 if not i: continue
200 for s in cover_sizes:
201 try:
202 s = int(s)
203 except Exception:
204 continue
205 w, h = i.size
206 sc = 1.0*s/max(w,h)
207 w = int(w*sc+0.5)
208 h = int(h*sc+0.5)
209 iw = i.resize((w,h),Image.ADAPTIVE)
210 filename = os.path.join(os.path.split(targetfile)[0],cover_out_filename.evaluate({'size':s}).encode(Config['fs_encoding'],Config['fs_encoding_err'] or 'replace'))
211 iw.save(filename)
212 outdirs.add(infile.meta['dir'])
214 def transcode_set(targetcodec,fileset,targetfiles,alsem,trsem,workdirs,workdirs_l):
215 try:
216 if not fileset:
217 workdirs_l = None
218 return
219 workdirs_l.acquire()
220 workdir, pipefiles = workdirs.pop()
221 workdirs_l.release()
222 outfiles = map(targetcodec._conv_out_filename,pipefiles[:len(fileset)])
223 if hasattr(targetcodec,'_from_wav_pipe_cmd'):
224 for i,p,o in zip(fileset,pipefiles,outfiles):
225 bgprocs = set()
226 dtask = get_codec(i).to_wav_pipe(i.meta['path'],p)
227 etask = targetcodec.from_wav_pipe(p,o)
228 ttask = FuncTask(background=True,target=transcode_track,args=(dtask,etask,trsem))
229 if trsem:
230 trsem.acquire()
231 bgprocs.add(ttask.run())
232 else:
233 ttask.runfg()
234 for task in bgprocs:
235 task.wait()
236 elif hasattr(targetcodec,'_from_wav_multi_cmd'):
237 etask = targetcodec.from_wav_multi(workdir,pipefiles[:len(fileset),outfiles])
238 etask.run()
239 for i,o in zip(fileset,pipefiles):
240 task = get_codec(i).to_wav_pipe(i.meta['path'])
241 task.run()
242 etask.wait()
243 dirs = set()
244 if hasattr(targetcodec,'_replaygain_cmd'):
245 targetcodec.add_replaygain(outfiles).run()
246 for i,o,t in zip(fileset,outfiles,targetfiles):
247 o = File(o)
248 o.meta = i.meta
249 o.save()
250 targetdir = os.path.split(t)[0]
251 if targetdir not in dirs:
252 dirs.add(targetdir)
253 if not os.path.isdir(targetdir):
254 os.makedirs(targetdir)
255 print "%s -> %s" %(i.filename,t)
256 util.move(o.filename,t)
257 check_and_copy_cover(fileset,targetfiles)
258 finally:
259 if workdirs_l:
260 workdirs_l.acquire()
261 workdirs.add((workdir,pipefiles))
262 workdirs_l.release()
263 if alsem:
264 alsem.release()
266 def sync_sets(sets=[]):
267 try:
268 semct = int(Config['jobs'])
269 except (ValueError,TypeError):
270 semct = 1
271 bgtasks = set()
272 targetcodec = Config['type']
273 if ',' in targetcodec:
274 allowedcodecs = targetcodec.split(',')
275 targetcodec = allowedcodecs[0]
276 allowedcodecs = set(allowedcodecs)
277 else:
278 allowedcodecs = set((targetcodec,))
279 targetcodec = get_codec(targetcodec)
280 workdir = Config['workdir'] or Config['base']
281 workdir = mkdtemp(dir=workdir,prefix='audiomangler_work_')
282 if hasattr(targetcodec,'_from_wav_pipe_cmd'):
283 if len(sets) > semct * 2:
284 alsem = BoundedSemaphore(semct)
285 trsem = None
286 else:
287 trsem = BoundedSemaphore(semct)
288 alsem = None
289 elif hasattr(targetcodec,'_from_wav_multi_cmd'):
290 trsem = None
291 alsem = BoundedSemaphore(semct)
292 numpipes = max(len(s) for s in sets)
293 workdirs = set()
294 workdirs_l = RLock()
295 for n in range(semct):
296 w = os.path.join(workdir,"%02d" % n)
297 os.mkdir(w)
298 pipes = []
299 for m in range(numpipes):
300 pipes.append(os.path.join(w,"%02d.wav"%m))
301 os.mkfifo(pipes[-1])
302 pipes = tuple(pipes)
303 workdirs.add((w,pipes))
304 for fileset in sets:
305 targetfiles = [f.format(postadd={'type':targetcodec.type_,'ext':targetcodec.ext}) for f in fileset]
306 if reduce(lambda x,y: x and os.path.isfile(y), targetfiles, True):
307 check_and_copy_cover(fileset,targetfiles)
308 continue
309 if reduce(lambda x,y: x and y.type_ in allowedcodecs, fileset, True):
310 targetfiles = [f.format() for f in fileset]
311 if not reduce(lambda x,y: x and os.path.isfile(y), targetfiles, True):
312 dirs = set()
313 for i in fileset:
314 t = i.format()
315 targetdir = os.path.split(t)[0]
316 if targetdir not in dirs:
317 dirs.add(targetdir)
318 if not os.path.isdir(targetdir):
319 os.makedirs(targetdir)
320 print "%s -> %s" % (i.filename,t)
321 util.copy(i.filename,t)
322 check_and_copy_cover(fileset,targetfiles)
323 continue
324 if alsem:
325 alsem.acquire()
326 for task in list(bgtasks):
327 if task.poll():
328 bgtasks.remove(task)
329 task = FuncTask(background=True,target=transcode_set,args=(targetcodec,fileset,targetfiles,alsem,trsem,workdirs,workdirs_l))
330 if alsem:
331 bgtasks.add(task.run())
332 else:
333 task.runfg()
334 for task in bgtasks:
335 task.wait()
336 for w,ps in workdirs:
337 for p in ps:
338 os.unlink(p)
339 os.rmdir(w)
340 os.rmdir(workdir)
342 def get_codec(item):
343 if isinstance(item, FileType):
344 item = getattr(item,'type_')
345 return codec_map[item]
347 __all__ = ['sync_sets']