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.

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