#!/usr/bin/env python # Copyright 2009 John-Mark Gurney '''Audio Raw Converter''' from DIDLLite import AudioItem, Album, Resource, ResourceList from FSStorage import FSObject, registerklassfun from twisted.web import resource, server from twisted.internet.interfaces import IPullProducer from zope.interface import implements decoders = {} try: import flac decoders['flac'] = flac.FLACDec except ImportError: pass mtformat = { 16: 'audio/l16', # BE signed # 8: 'audio/l8', unsigned } def makeaudiomt(bitsps, rate, nchan): try: mt = mtformat[bitsps] except KeyError: raise KeyError('No mime-type for audio format: %s.' % `bitsps`) return '%s;rate=%d;channels=%d' % (mt, rate, nchan) def makemtfromdec(dec): return makeaudiomt(dec.bitspersample, dec.samplerate, dec.channels) class DecoderProducer: implements(IPullProducer) def __init__(self, consumer, decoder, tbytes, skipbytes): '''skipbytes should always be small. It is here in case someone requests the middle of a sample.''' self.decoder = decoder self.consumer = consumer self.tbytes = tbytes self.skipbytes = skipbytes #print 'DPregP', `self`, `self.tbytes`, `self.skipbytes` consumer.registerProducer(self, False) self.resumeProducing() def pauseProducing(self): # XXX - bug in Twisted 8.2.0 on pipelined requests this is # called: http://twistedmatrix.com/trac/ticket/3919 pass def resumeProducing(self): #print 'DPrP', `self` r = self.decoder.read(oneblk=True) if r: #print 'DPrP:', len(r) if self.skipbytes: cnt = min(self.skipbytes, len(r)) r = r[cnt:] self.skipbytes -= cnt send = min(len(r), self.tbytes) r = r[:send] self.tbytes -= len(r) self.consumer.write(r) #print 'write %d bytes, remaining %d' % (len(r), self.tbytes) if self.tbytes: return #print 'DPurP', `self` self.consumer.unregisterProducer() self.consumer.finish() def stopProducing(self): #print 'DPsP', `self` self.decoder.close() self.decoder = None self.consumer = None class AudioResource(resource.Resource): isLeaf = True def __init__(self, f, dec, start, cnt): resource.Resource.__init__(self) self.f = f self.dec = dec self.start = start self.cnt = cnt def calcrange(self, rng, l): rng = rng.strip() unit, rangeset = rng.split('=') assert unit == 'bytes', `unit` start, end = rangeset.split('-') start = int(start) if end: end = int(end) else: end = l return start, end - start + 1 def render(self, request): #print 'render:', `request` decoder = self.dec(self.f) request.setHeader('content-type', makemtfromdec(decoder)) bytespersample = decoder.channels * decoder.bitspersample / 8 tbytes = self.cnt * bytespersample skipbytes = 0 request.setHeader('content-length', tbytes) request.setHeader('accept-ranges', 'bytes') if request.requestHeaders.hasHeader('range'): #print 'range req:', `request.requestHeaders.getRawHeaders('range')` start, cnt = self.calcrange( request.requestHeaders.getRawHeaders('range')[0], tbytes) skipbytes = start % bytespersample #print 'going:', start / bytespersample decoder.goto(self.start + start / bytespersample) #print 'there' request.setHeader('content-length', cnt) request.setHeader('content-range', 'bytes %s-%s/%s' % (start, start + cnt - 1, tbytes)) tbytes = cnt else: decoder.goto(self.start) if request.method == 'HEAD': return '' DecoderProducer(request, decoder, tbytes, skipbytes) #print 'producing render', `decoder`, `tbytes`, `skipbytes` # and make sure the connection doesn't get closed return server.NOT_DONE_YET # XXX - maybe should be MusicAlbum, but needs to change AudioRaw class AudioDisc(FSObject, Album): def __init__(self, *args, **kwargs): self.cuesheet = kwargs.pop('cuesheet') self.kwargs = kwargs.copy() self.file = kwargs.pop('file') nchan = kwargs['channels'] samprate = kwargs['samplerate'] bitsps = kwargs['bitspersample'] samples = kwargs['samples'] totalbytes = nchan * samples * bitsps / 8 FSObject.__init__(self, kwargs['path']) # XXX - exclude track 1 pre-gap? kwargs['content'] = AudioResource(self.file, kwargs.pop('decoder'), 0, kwargs['samples']) #print 'doing construction' Album.__init__(self, *args, **kwargs) #print 'adding resource' self.url = '%s/%s' % (self.cd.urlbase, self.id) self.res = ResourceList() r = Resource(self.url, 'http-get:*:%s:*' % makeaudiomt(bitsps, samprate, nchan)) r.size = totalbytes r.duration = float(samples) / samprate r.bitrate = nchan * samprate * bitsps / 8 r.sampleFrequency = samprate r.bitsPerSample = bitsps r.nrAudioChannels = nchan self.res.append(r) #print 'completed' def sort(self, fun=lambda x, y: cmp(int(x.title), int(y.title))): return list.sort(self, fun) def genChildren(self): r = [ str(x['number']) for x in self.cuesheet['tracks_array'] if x['number'] not in (170, 255) ] #print 'gC:', `r` return r def findtrackidx(self, trk): for idx, i in enumerate(self.cuesheet['tracks_array']): if i['number'] == trk: return idx raise ValueError('track %d not found' % trk) @staticmethod def findindexintrack(trk, idx): for i in trk['indices_array']: if i['number'] == idx: return i raise ValueError('index %d not found in: %s' % (idx, trk)) def gettrackstart(self, i): idx = self.findtrackidx(i) track = self.cuesheet['tracks_array'][idx] index = self.findindexintrack(track, 1) return track['offset'] + index['offset'] def createObject(self, i, arg=None): '''This function returns the (class, name, *args, **kwargs) that will be passed to the addItem method of the ContentDirectory. arg will be passed the value of the dict keyed by i if genChildren is a dict.''' oi = i i = int(i) trkidx = self.findtrackidx(i) trkarray = self.cuesheet['tracks_array'] kwargs = self.kwargs.copy() start = self.gettrackstart(i) kwargs['start'] = start kwargs['samples'] = trkarray[trkidx + 1]['offset'] - start return AudioRaw, oi, (), kwargs class AudioRaw(AudioItem, FSObject): def __init__(self, *args, **kwargs): file = kwargs.pop('file') nchan = kwargs.pop('channels') samprate = kwargs.pop('samplerate') bitsps = kwargs.pop('bitspersample') samples = kwargs.pop('samples') startsamp = kwargs.pop('start', 0) totalbytes = nchan * samples * bitsps / 8 FSObject.__init__(self, kwargs['path']) kwargs['content'] = AudioResource(file, kwargs.pop('decoder'), startsamp, startsamp + samples) AudioItem.__init__(self, *args, **kwargs) self.url = '%s/%s' % (self.cd.urlbase, self.id) self.res = ResourceList() r = Resource(self.url, 'http-get:*:%s:*' % makeaudiomt(bitsps, samprate, nchan)) r.size = totalbytes r.duration = float(samples) / samprate r.bitrate = nchan * samprate * bitsps / 8 r.sampleFrequency = samprate r.bitsPerSample = bitsps r.nrAudioChannels = nchan self.res.append(r) def detectaudioraw(origpath, fobj): for i in decoders.itervalues(): try: obj = i(origpath) # XXX - don't support down sampling yet if obj.bitspersample not in (8, 16): continue args = { 'path': origpath, 'decoder': i, 'file': origpath, 'channels': obj.channels, 'samplerate': obj.samplerate, 'bitspersample': obj.bitspersample, 'samples': obj.totalsamples, } if obj.cuesheet is not None: args['cuesheet'] = obj.cuesheet return AudioDisc, args return AudioRaw, args except: #import traceback #traceback.print_exc() pass return None, None registerklassfun(detectaudioraw, True)