A Python UPnP Media Server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

295 lines
7.8 KiB

  1. #!/usr/bin/env python
  2. # Copyright 2009 John-Mark Gurney <jmg@funkthat.com>
  3. '''Audio Raw Converter'''
  4. from DIDLLite import AudioItem, Album, Resource, ResourceList
  5. from FSStorage import FSObject, registerklassfun
  6. from twisted.web import resource, server
  7. from twisted.internet.interfaces import IPullProducer
  8. from zope.interface import implements
  9. decoders = {}
  10. try:
  11. import flac
  12. decoders['flac'] = flac.FLACDec
  13. except ImportError:
  14. pass
  15. mtformat = {
  16. 16: 'audio/l16', # BE signed
  17. # 8: 'audio/l8', unsigned
  18. }
  19. def makeaudiomt(bitsps, rate, nchan):
  20. try:
  21. mt = mtformat[bitsps]
  22. except KeyError:
  23. raise KeyError('No mime-type for audio format: %s.' %
  24. `bitsps`)
  25. return '%s;rate=%d;channels=%d' % (mt, rate, nchan)
  26. def makemtfromdec(dec):
  27. return makeaudiomt(dec.bitspersample, dec.samplerate,
  28. dec.channels)
  29. class DecoderProducer:
  30. implements(IPullProducer)
  31. def __init__(self, consumer, decoder, tbytes, skipbytes):
  32. '''skipbytes should always be small. It is here in case
  33. someone requests the middle of a sample.'''
  34. self.decoder = decoder
  35. self.consumer = consumer
  36. self.tbytes = tbytes
  37. self.skipbytes = skipbytes
  38. #print 'DPregP', `self`, `self.tbytes`, `self.skipbytes`
  39. consumer.registerProducer(self, False)
  40. self.resumeProducing()
  41. def __repr__(self):
  42. return '<DecoderProducer: decoder: %s, bytes left: %d, skip: %d>' % (`self.decoder`, self.tbytes, self.skipbytes)
  43. def pauseProducing(self):
  44. # XXX - bug in Twisted 8.2.0 on pipelined requests this is
  45. # called: http://twistedmatrix.com/trac/ticket/3919
  46. pass
  47. def resumeProducing(self):
  48. #print 'DPrP', `self`
  49. r = self.decoder.read(256*1024)
  50. if r:
  51. #print 'DPrP:', len(r)
  52. if self.skipbytes:
  53. cnt = min(self.skipbytes, len(r))
  54. r = r[cnt:]
  55. self.skipbytes -= cnt
  56. send = min(len(r), self.tbytes)
  57. r = r[:send]
  58. self.tbytes -= len(r)
  59. self.consumer.write(r)
  60. #print 'write %d bytes, remaining %d' % (len(r), self.tbytes)
  61. if self.tbytes:
  62. return
  63. #print 'DPurP', `self`
  64. self.consumer.unregisterProducer()
  65. self.consumer.finish()
  66. def stopProducing(self):
  67. #print 'DPsP', `self`
  68. self.decoder.close()
  69. self.decoder = None
  70. self.consumer = None
  71. class AudioResource(resource.Resource):
  72. isLeaf = True
  73. def __init__(self, f, dec, start, cnt):
  74. resource.Resource.__init__(self)
  75. self.f = f
  76. self.dec = dec
  77. self.start = start
  78. self.cnt = cnt
  79. def calcrange(self, rng, l):
  80. rng = rng.strip()
  81. unit, rangeset = rng.split('=')
  82. assert unit == 'bytes', `unit`
  83. start, end = rangeset.split('-')
  84. start = int(start)
  85. if end:
  86. end = int(end)
  87. else:
  88. end = l
  89. return start, end - start + 1
  90. def render(self, request):
  91. #print 'render:', `request`
  92. decoder = self.dec(self.f)
  93. request.setHeader('content-type', makemtfromdec(decoder))
  94. bytespersample = decoder.channels * decoder.bitspersample / 8
  95. tbytes = self.cnt * bytespersample
  96. #print 'tbytes:', `tbytes`, 'cnt:', `self.cnt`
  97. skipbytes = 0
  98. request.setHeader('content-length', tbytes)
  99. request.setHeader('accept-ranges', 'bytes')
  100. if request.requestHeaders.hasHeader('range'):
  101. #print 'range req:', `request.requestHeaders.getRawHeaders('range')`
  102. start, cnt = self.calcrange(
  103. request.requestHeaders.getRawHeaders('range')[0],
  104. tbytes)
  105. skipbytes = start % bytespersample
  106. #print 'going:', start / bytespersample
  107. decoder.goto(self.start + start / bytespersample)
  108. #print 'there'
  109. request.setHeader('content-length', cnt)
  110. request.setHeader('content-range', 'bytes %s-%s/%s' %
  111. (start, start + cnt - 1, tbytes))
  112. tbytes = cnt
  113. else:
  114. decoder.goto(self.start)
  115. if request.method == 'HEAD':
  116. return ''
  117. DecoderProducer(request, decoder, tbytes, skipbytes)
  118. #print 'producing render', `decoder`, `tbytes`, `skipbytes`
  119. # and make sure the connection doesn't get closed
  120. return server.NOT_DONE_YET
  121. # XXX - maybe should be MusicAlbum, but needs to change AudioRaw
  122. class AudioDisc(FSObject, Album):
  123. def __init__(self, *args, **kwargs):
  124. self.cuesheet = kwargs.pop('cuesheet')
  125. self.kwargs = kwargs.copy()
  126. self.file = kwargs.pop('file')
  127. nchan = kwargs['channels']
  128. samprate = kwargs['samplerate']
  129. bitsps = kwargs['bitspersample']
  130. samples = kwargs['samples']
  131. totalbytes = nchan * samples * bitsps / 8
  132. FSObject.__init__(self, kwargs['path'])
  133. # XXX - exclude track 1 pre-gap?
  134. kwargs['content'] = AudioResource(self.file,
  135. kwargs.pop('decoder'), 0, kwargs['samples'])
  136. #print 'doing construction'
  137. Album.__init__(self, *args, **kwargs)
  138. #print 'adding resource'
  139. self.url = '%s/%s' % (self.cd.urlbase, self.id)
  140. self.res = ResourceList()
  141. r = Resource(self.url, 'http-get:*:%s:*' % makeaudiomt(bitsps,
  142. samprate, nchan))
  143. r.size = totalbytes
  144. r.duration = float(samples) / samprate
  145. r.bitrate = nchan * samprate * bitsps / 8
  146. r.sampleFrequency = samprate
  147. r.bitsPerSample = bitsps
  148. r.nrAudioChannels = nchan
  149. self.res.append(r)
  150. #print 'completed'
  151. def sort(self, fun=lambda x, y: cmp(int(x.title), int(y.title))):
  152. return list.sort(self, fun)
  153. def genChildren(self):
  154. r = [ str(x['number']) for x in
  155. self.cuesheet['tracks_array'] if x['number'] not in
  156. (170, 255) ]
  157. #print 'gC:', `r`
  158. return r
  159. def findtrackidx(self, trk):
  160. for idx, i in enumerate(self.cuesheet['tracks_array']):
  161. if i['number'] == trk:
  162. return idx
  163. raise ValueError('track %d not found' % trk)
  164. @staticmethod
  165. def findindexintrack(trk, idx):
  166. for i in trk['indices_array']:
  167. if i['number'] == idx:
  168. return i
  169. raise ValueError('index %d not found in: %s' % (idx, trk))
  170. def gettrackstart(self, i):
  171. idx = self.findtrackidx(i)
  172. track = self.cuesheet['tracks_array'][idx]
  173. index = self.findindexintrack(track, 1)
  174. return track['offset'] + index['offset']
  175. def createObject(self, i, arg=None):
  176. '''This function returns the (class, name, *args, **kwargs)
  177. that will be passed to the addItem method of the
  178. ContentDirectory. arg will be passed the value of the dict
  179. keyed by i if genChildren is a dict.'''
  180. oi = i
  181. i = int(i)
  182. trkidx = self.findtrackidx(i)
  183. trkarray = self.cuesheet['tracks_array']
  184. kwargs = self.kwargs.copy()
  185. start = self.gettrackstart(i)
  186. kwargs['start'] = start
  187. kwargs['samples'] = trkarray[trkidx + 1]['offset'] - start
  188. #print 'track: %d, kwargs: %s' % (i, `kwargs`)
  189. return AudioRaw, oi, (), kwargs
  190. class AudioRaw(AudioItem, FSObject):
  191. def __init__(self, *args, **kwargs):
  192. file = kwargs.pop('file')
  193. nchan = kwargs.pop('channels')
  194. samprate = kwargs.pop('samplerate')
  195. bitsps = kwargs.pop('bitspersample')
  196. samples = kwargs.pop('samples')
  197. startsamp = kwargs.pop('start', 0)
  198. totalbytes = nchan * samples * bitsps / 8
  199. FSObject.__init__(self, kwargs['path'])
  200. #print 'AudioRaw:', `startsamp`, `samples`
  201. kwargs['content'] = AudioResource(file,
  202. kwargs.pop('decoder'), startsamp, samples)
  203. AudioItem.__init__(self, *args, **kwargs)
  204. self.url = '%s/%s' % (self.cd.urlbase, self.id)
  205. self.res = ResourceList()
  206. r = Resource(self.url, 'http-get:*:%s:*' % makeaudiomt(bitsps,
  207. samprate, nchan))
  208. r.size = totalbytes
  209. r.duration = float(samples) / samprate
  210. r.bitrate = nchan * samprate * bitsps / 8
  211. r.sampleFrequency = samprate
  212. r.bitsPerSample = bitsps
  213. r.nrAudioChannels = nchan
  214. self.res.append(r)
  215. def detectaudioraw(origpath, fobj):
  216. for i in decoders.itervalues():
  217. try:
  218. obj = i(origpath)
  219. # XXX - don't support down sampling yet
  220. if obj.bitspersample not in (8, 16):
  221. continue
  222. args = {
  223. 'path': origpath,
  224. 'decoder': i,
  225. 'file': origpath,
  226. 'channels': obj.channels,
  227. 'samplerate': obj.samplerate,
  228. 'bitspersample': obj.bitspersample,
  229. 'samples': obj.totalsamples,
  230. }
  231. if obj.cuesheet is not None:
  232. args['cuesheet'] = obj.cuesheet
  233. return AudioDisc, args
  234. return AudioRaw, args
  235. except:
  236. #import traceback
  237. #traceback.print_exc()
  238. pass
  239. return None, None
  240. registerklassfun(detectaudioraw, True)