|
- #!/usr/bin/env python
- # Copyright 2009 John-Mark Gurney <jmg@funkthat.com>
-
- '''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 __repr__(self):
- return '<DecoderProducer: decoder: %s, bytes left: %d, skip: %d>' % (`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 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
-
- # 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
- #print 'track: %d, kwargs: %s' % (i, `kwargs`)
-
- 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'])
-
- #print 'AudioRaw:', `startsamp`, `samples`
- kwargs['content'] = AudioResource(file,
- kwargs.pop('decoder'), 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)
|