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.

316 lines
8.4 KiB

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