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.

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