#!/usr/bin/env python # Copyright 2009 John-Mark Gurney '''Audio Raw Converter''' import cdrtoc from DIDLLite import MusicTrack, AudioItem, MusicAlbum, Resource, ResourceList from FSStorage import FSObject, registerklassfun from twisted.web import resource, server, static 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 __repr__(self): return '' % (`self.decoder`, self.tbytes, self.skipbytes) 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(256*1024) 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 __repr__(self): return '' % (`self.f`, self.dec, self.start, self.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 #print 'tbytes:', `tbytes`, 'cnt:', `self.cnt` 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 class AudioDisc(FSObject, MusicAlbum): def __init__(self, *args, **kwargs): self.cuesheet = kwargs.pop('cuesheet') self.cdtoc = kwargs.pop('toc', {}) self.kwargs = kwargs.copy() self.file = kwargs.pop('file') nchan = kwargs.pop('channels') samprate = kwargs.pop('samplerate') bitsps = kwargs.pop('bitspersample') samples = kwargs.pop('samples') tags = kwargs.pop('tags', {}) picts = kwargs.pop('pictures', {}) totalbytes = nchan * samples * bitsps / 8 FSObject.__init__(self, kwargs.pop('path')) # XXX - exclude track 1 pre-gap? kwargs['content'] = cont = resource.Resource() cont.putChild('audio', AudioResource(file, kwargs.pop('decoder'), 0, samples)) #print 'doing construction' MusicAlbum.__init__(self, *args, **kwargs) #print 'adding resource' self.url = '%s/%s/audio' % (self.cd.urlbase, self.id) if 'cover' in picts: pict = picts['cover'][0] #print 'p:', `pict` cont.putChild('cover', static.Data(pict[7], pict[1])) self.albumArtURI = '%s/%s/cover' % (self.cd.urlbase, self.id) self.res = ResourceList() if 'DYEAR' in tags: self.year = tags['DYEAR'] if 'DGENRE' in tags: self.genre = tags['DGENRE'] #if 'DTITLE' in tags: # self.artist, self.album = tags['DTITLE'][0].split(' / ', 1) self.url = '%s/%s/audio' % (self.cd.urlbase, self.id) 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.originalTrackNumber), int(y.originalTrackNumber))): 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 #print 'track: %d, kwargs: %s' % (i, `kwargs`) kwargs['originalTrackNumber'] = i try: tinfo = self.cdtoc['tracks'][i] except KeyError: tinfo = {} if 'TITLE' in self.cdtoc: kwargs['album'] = self.cdtoc['TITLE'] if 'TITLE' in tinfo: oi = tinfo['TITLE'] if 'PERFORMER' in tinfo: kwargs['artist'] = tinfo['PERFORMER'] print 'kwargs:', `kwargs` import traceback traceback.print_stack() return AudioRawTrack, oi, (), kwargs # XXX - figure out how to make custom mix-ins w/ other than AudioItem # metaclass? class AudioRawBase(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) tags = kwargs.pop('tags', {}) picts = kwargs.pop('pictures', {}) totalbytes = nchan * samples * bitsps / 8 FSObject.__init__(self, kwargs.pop('path')) #print 'AudioRaw:', `startsamp`, `samples` kwargs['content'] = cont = resource.Resource() cont.putChild('audio', AudioResource(file, kwargs.pop('decoder'), startsamp, samples)) self.baseObject.__init__(self, *args, **kwargs) if 'DYEAR' in tags: self.year = tags['DYEAR'][0] if 'DGENRE' in tags: self.genre = tags['DGENRE'][0] #if 'DTITLE' in tags: # self.artist, self.album = tags['DTITLE'][0].split(' / ', 1) self.url = '%s/%s/audio' % (self.cd.urlbase, self.id) if 'cover' in picts: pict = picts['cover'][0] cont.putChild('cover', static.Data(pict[7], pict[1])) self.albumArtURI = '%s/%s/cover' % (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 doUpdate(self): print 'dU:', `self`, self.baseObject.doUpdate print self.__class__.__bases__ import traceback traceback.print_stack() self.baseObject.doUpdate(self) class AudioRaw(AudioRawBase, AudioItem): baseObject = AudioItem class AudioRawTrack(AudioRawBase, MusicTrack): baseObject = MusicTrack 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, 'tags': obj.tags, 'pictures': obj.pictures, } if obj.cuesheet is not None: try: args['toc'] = cdrtoc.parsetoc( obj.tags['cd.toc'][0]) except KeyError: pass except: import traceback print 'WARNING: failed to parse toc:' traceback.print_exc() args['cuesheet'] = obj.cuesheet return AudioDisc, args return AudioRaw, args except: #import traceback #traceback.print_exc() pass return None, None registerklassfun(detectaudioraw, True)