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.

292 lines
7.7 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. #print 'tbytes:', `tbytes`, 'cnt:', `self.cnt`
  95. skipbytes = 0
  96. request.setHeader('content-length', tbytes)
  97. request.setHeader('accept-ranges', 'bytes')
  98. if request.requestHeaders.hasHeader('range'):
  99. #print 'range req:', `request.requestHeaders.getRawHeaders('range')`
  100. start, cnt = self.calcrange(
  101. request.requestHeaders.getRawHeaders('range')[0],
  102. tbytes)
  103. skipbytes = start % bytespersample
  104. #print 'going:', start / bytespersample
  105. decoder.goto(self.start + start / bytespersample)
  106. #print 'there'
  107. request.setHeader('content-length', cnt)
  108. request.setHeader('content-range', 'bytes %s-%s/%s' %
  109. (start, start + cnt - 1, tbytes))
  110. tbytes = cnt
  111. else:
  112. decoder.goto(self.start)
  113. if request.method == 'HEAD':
  114. return ''
  115. DecoderProducer(request, decoder, tbytes, skipbytes)
  116. #print 'producing render', `decoder`, `tbytes`, `skipbytes`
  117. # and make sure the connection doesn't get closed
  118. return server.NOT_DONE_YET
  119. # XXX - maybe should be MusicAlbum, but needs to change AudioRaw
  120. class AudioDisc(FSObject, Album):
  121. def __init__(self, *args, **kwargs):
  122. self.cuesheet = kwargs.pop('cuesheet')
  123. self.kwargs = kwargs.copy()
  124. self.file = kwargs.pop('file')
  125. nchan = kwargs['channels']
  126. samprate = kwargs['samplerate']
  127. bitsps = kwargs['bitspersample']
  128. samples = kwargs['samples']
  129. totalbytes = nchan * samples * bitsps / 8
  130. FSObject.__init__(self, kwargs['path'])
  131. # XXX - exclude track 1 pre-gap?
  132. kwargs['content'] = AudioResource(self.file,
  133. kwargs.pop('decoder'), 0, kwargs['samples'])
  134. #print 'doing construction'
  135. Album.__init__(self, *args, **kwargs)
  136. #print 'adding resource'
  137. self.url = '%s/%s' % (self.cd.urlbase, self.id)
  138. self.res = ResourceList()
  139. r = Resource(self.url, 'http-get:*:%s:*' % makeaudiomt(bitsps,
  140. samprate, nchan))
  141. r.size = totalbytes
  142. r.duration = float(samples) / samprate
  143. r.bitrate = nchan * samprate * bitsps / 8
  144. r.sampleFrequency = samprate
  145. r.bitsPerSample = bitsps
  146. r.nrAudioChannels = nchan
  147. self.res.append(r)
  148. #print 'completed'
  149. def sort(self, fun=lambda x, y: cmp(int(x.title), int(y.title))):
  150. return list.sort(self, fun)
  151. def genChildren(self):
  152. r = [ str(x['number']) for x in
  153. self.cuesheet['tracks_array'] if x['number'] not in
  154. (170, 255) ]
  155. #print 'gC:', `r`
  156. return r
  157. def findtrackidx(self, trk):
  158. for idx, i in enumerate(self.cuesheet['tracks_array']):
  159. if i['number'] == trk:
  160. return idx
  161. raise ValueError('track %d not found' % trk)
  162. @staticmethod
  163. def findindexintrack(trk, idx):
  164. for i in trk['indices_array']:
  165. if i['number'] == idx:
  166. return i
  167. raise ValueError('index %d not found in: %s' % (idx, trk))
  168. def gettrackstart(self, i):
  169. idx = self.findtrackidx(i)
  170. track = self.cuesheet['tracks_array'][idx]
  171. index = self.findindexintrack(track, 1)
  172. return track['offset'] + index['offset']
  173. def createObject(self, i, arg=None):
  174. '''This function returns the (class, name, *args, **kwargs)
  175. that will be passed to the addItem method of the
  176. ContentDirectory. arg will be passed the value of the dict
  177. keyed by i if genChildren is a dict.'''
  178. oi = i
  179. i = int(i)
  180. trkidx = self.findtrackidx(i)
  181. trkarray = self.cuesheet['tracks_array']
  182. kwargs = self.kwargs.copy()
  183. start = self.gettrackstart(i)
  184. kwargs['start'] = start
  185. kwargs['samples'] = trkarray[trkidx + 1]['offset'] - start
  186. #print 'track: %d, kwargs: %s' % (i, `kwargs`)
  187. return AudioRaw, oi, (), kwargs
  188. class AudioRaw(AudioItem, FSObject):
  189. def __init__(self, *args, **kwargs):
  190. file = kwargs.pop('file')
  191. nchan = kwargs.pop('channels')
  192. samprate = kwargs.pop('samplerate')
  193. bitsps = kwargs.pop('bitspersample')
  194. samples = kwargs.pop('samples')
  195. startsamp = kwargs.pop('start', 0)
  196. totalbytes = nchan * samples * bitsps / 8
  197. FSObject.__init__(self, kwargs['path'])
  198. #print 'AudioRaw:', `startsamp`, `samples`
  199. kwargs['content'] = AudioResource(file,
  200. kwargs.pop('decoder'), startsamp, samples)
  201. AudioItem.__init__(self, *args, **kwargs)
  202. self.url = '%s/%s' % (self.cd.urlbase, self.id)
  203. self.res = ResourceList()
  204. r = Resource(self.url, 'http-get:*:%s:*' % makeaudiomt(bitsps,
  205. samprate, nchan))
  206. r.size = totalbytes
  207. r.duration = float(samples) / samprate
  208. r.bitrate = nchan * samprate * bitsps / 8
  209. r.sampleFrequency = samprate
  210. r.bitsPerSample = bitsps
  211. r.nrAudioChannels = nchan
  212. self.res.append(r)
  213. def detectaudioraw(origpath, fobj):
  214. for i in decoders.itervalues():
  215. try:
  216. obj = i(origpath)
  217. # XXX - don't support down sampling yet
  218. if obj.bitspersample not in (8, 16):
  219. continue
  220. args = {
  221. 'path': origpath,
  222. 'decoder': i,
  223. 'file': origpath,
  224. 'channels': obj.channels,
  225. 'samplerate': obj.samplerate,
  226. 'bitspersample': obj.bitspersample,
  227. 'samples': obj.totalsamples,
  228. }
  229. if obj.cuesheet is not None:
  230. args['cuesheet'] = obj.cuesheet
  231. return AudioDisc, args
  232. return AudioRaw, args
  233. except:
  234. #import traceback
  235. #traceback.print_exc()
  236. pass
  237. return None, None
  238. registerklassfun(detectaudioraw, True)