A WebRTC based tool to support low latency audio conferencing. This is targeted to allow musicians to be able to jam together.
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.
 
 
 

243 lines
6.6 KiB

  1. #!/usr/bin/env python
  2. from ctypes import c_int, pointer, POINTER, sizeof, c_char, c_uint8, c_int16, cast
  3. from pyogg import opus
  4. from pyogg.opus import opus_int16, opus_int16_p, c_uchar
  5. import array
  6. import functools
  7. import operator
  8. import pyaudio
  9. import queue
  10. import sys
  11. import time
  12. import wave
  13. statusvals = [ 'paInputUnderflow', 'paInputOverflow', 'paOutputUnderflow', 'paOutputOverflow', 'paPrimingOutput' ]
  14. statuses = { getattr(pyaudio, y): y for y in statusvals }
  15. def printstatus(x):
  16. r = []
  17. while x:
  18. b = x & -x # get a single bit
  19. r.append(statuses[b])
  20. x &= ~b # clear the bit we added
  21. return ', '.join(r)
  22. rate = 48000
  23. framelength = .0025
  24. maxbw = 128*1024
  25. sampperframe = int(framelength * rate) # 5ms latency
  26. pa = pyaudio.PyAudio()
  27. for i in range(pa.get_device_count()):
  28. print(i, repr(pa.get_device_info_by_index(i)))
  29. #sys.exit(0)
  30. class _OpusBase(object):
  31. @staticmethod
  32. def _check_err(err):
  33. if err != opus.OPUS_OK:
  34. raise RuntimeError('failed', err)
  35. class OpusDecoder(_OpusBase):
  36. def __init__(self, rate, nchan):
  37. err = c_int()
  38. self._nchan = nchan
  39. self._bytespersamp = nchan * sizeof(opus.opus_int16)
  40. #print('foo:', repr((self._bytespersamp, nchan, sizeof(opus.opus_int16))))
  41. self._pcmbuf = (opus_int16 * (nchan * int(rate * framelength)))()
  42. self._dec = opus.opus_decoder_create(rate, nchan, pointer(err))
  43. self._check_err(err.value)
  44. def decode(self, frm):
  45. #print(repr(frm))
  46. r = opus.opus_decode(self._dec, cast(frm, POINTER(c_uchar)), len(frm), self._pcmbuf, len(self._pcmbuf), 0)
  47. if r < 0:
  48. self._check_err(r)
  49. return array.array('h', self._pcmbuf[:self._nchan * r]).tobytes()
  50. def __del__(self):
  51. opus.opus_decoder_destroy(self._dec)
  52. self._dec = None
  53. class OpusEncoder(_OpusBase):
  54. def __init__(self, rate, nchan, app):
  55. err = c_int()
  56. self._nchan = nchan
  57. self._bytespersamp = nchan * sizeof(opus.opus_int16)
  58. #print('bar:', repr((self._bytespersamp, nchan, sizeof(opus.opus_int16))))
  59. self._frbuf = (c_char * (self._bytespersamp * int(maxbw * framelength)))()
  60. self._enc = opus.opus_encoder_create(rate, nchan, app, pointer(err))
  61. self._check_err(err.value)
  62. def get_max_bw(self):
  63. val = opus.opus_int32()
  64. r = opus.opus_encoder_ctl(self._enc, opus.OPUS_GET_MAX_BANDWIDTH, pointer(val))
  65. self._check_err(r)
  66. return val.value
  67. def set_max_bw(self, maxbw):
  68. r = opus.opus_encoder_ctl(self._enc, opus.OPUS_SET_MAX_BANDWIDTH, opus.opus_int32(maxbw))
  69. self._check_err(r)
  70. def get_inband_fec(self):
  71. val = opus.opus_int32()
  72. r = opus.opus_encoder_ctl(self._enc, opus.OPUS_GET_INBAND_FEC_REQUEST, pointer(val))
  73. self._check_err(r)
  74. return val.value
  75. def set_inband_fec(self, maxbw):
  76. r = opus.opus_encoder_ctl(self._enc, opus.OPUS_SET_INBAND_FEC_REQUEST, opus.opus_int32(maxbw))
  77. self._check_err(r)
  78. def get_pkt_loss(self):
  79. val = opus.opus_int32()
  80. r = opus.opus_encoder_ctl(self._enc, opus.OPUS_GET_PACKET_LOSS_PERC_REQUEST, pointer(val))
  81. self._check_err(r)
  82. return val.value
  83. def set_pkt_loss(self, percent):
  84. r = opus.opus_encoder_ctl(self._enc, opus.OPUS_SET_PACKET_LOSS_PERC_REQUEST, opus.opus_int32(percent))
  85. self._check_err(r)
  86. def encode(self, pcm):
  87. fs = len(pcm) // self._bytespersamp
  88. #print(repr(pcm), fs, repr(opus_int16_p), repr(self._nchan))
  89. #print('baz:', fs, repr((len(pcm), self._bytespersamp)))
  90. r = opus.opus_encode(self._enc, cast(pcm, opus_int16_p), fs, cast(self._frbuf, POINTER(c_uchar)), len(self._frbuf))
  91. #r = opus.opus_encode(self._enc, pcm, fs, self._frbuf, len(self._frbuf))
  92. if r < 0:
  93. self._check_err(r)
  94. #print(repr(self._frbuf), dir(self._frbuf))
  95. return self._frbuf.raw[:r]
  96. def __del__(self):
  97. opus.opus_encoder_destroy(self._enc)
  98. self._enc = None
  99. enc = OpusEncoder(rate, 1, opus.OPUS_APPLICATION_RESTRICTED_LOWDELAY)
  100. dec = OpusDecoder(rate, 1)
  101. enc.set_inband_fec(1)
  102. enc.set_pkt_loss(5)
  103. print('pl:', repr(enc.get_pkt_loss()))
  104. print('if:', repr(enc.get_inband_fec()))
  105. #sys.exit(0)
  106. inbuffer = queue.Queue()
  107. outbufferinfo = []
  108. outbuffer = []
  109. adcdiff = []
  110. dacdiff = []
  111. adcdacdiff = []
  112. times = []
  113. def excprinter(func):
  114. @functools.wraps(func)
  115. def func_wrapper(*args, **kwargs):
  116. global times
  117. try:
  118. s = time.time()
  119. r = func(*args, **kwargs)
  120. times.append((s, time.time()))
  121. except:
  122. import traceback
  123. traceback.print_exc()
  124. raise
  125. return r
  126. return func_wrapper
  127. #wf = wave.open('foo.wav', 'wb')
  128. #wf.setnchannels(1)
  129. #wf.setsampwidth(2)
  130. #wf.setframerate(rate)
  131. cnt = 0
  132. starttime = None
  133. combstatus = 0
  134. @excprinter
  135. def incallback(in_data, frm_cnt, timeinfo, status):
  136. inbuffer.put((in_data, frm_cnt, timeinfo, status))
  137. if time.time() - s < 5:
  138. return ('', pyaudio.paContinue)
  139. inbuffer.put(None) # signal finished
  140. return ('', pyaudio.paComplete)
  141. def outcallback(in_data, frm_cnt, timeinfo, status):
  142. outbufferinfo.append((in_data, frm_cnt, timeinfo, status))
  143. if outbuffer:
  144. buf = outbuffer.pop(0)
  145. else:
  146. buf = '\x00\x00' * sampperframe
  147. if buf is None:
  148. return (dbuf, pyaudio.paComplete)
  149. return (buf, pyaudio.paContinue)
  150. instream = pa.open(rate=rate, channels=1, format=pyaudio.paInt16, input_device_index=0, input=True, frames_per_buffer=sampperframe, stream_callback=incallback)
  151. outstream = pa.open(rate=rate, channels=1, format=pyaudio.paInt16, output_device_index=3, output=True, frames_per_buffer=sampperframe, stream_callback=outcallback)
  152. print('il:', instream.get_input_latency())
  153. print('ol:', outstream.get_output_latency())
  154. s = time.time()
  155. print('starting')
  156. instream.start_stream()
  157. outstream.start_stream()
  158. while True:
  159. #print('sleep')
  160. d = inbuffer.get()
  161. if d is None:
  162. break
  163. in_data, frm_cnt, timeinfo, status = d
  164. combstatus |= status
  165. if starttime is None:
  166. starttime = timeinfo
  167. lasttime = timeinfo
  168. #print('pcb:', repr((type(in_data), len(in_data), frm_cnt, timeinfo, status)))
  169. adcdiff.append(timeinfo['current_time'] - timeinfo['input_buffer_adc_time'])
  170. dacdiff.append(timeinfo['output_buffer_dac_time'] - timeinfo['current_time'])
  171. adcdacdiff.append(timeinfo['output_buffer_dac_time'] - timeinfo['input_buffer_adc_time'])
  172. cnt += len(in_data)
  173. #buf = enc.encode(in_data)
  174. buf = in_data
  175. #print('r:', len(buf), repr(buf), repr(type(buf)))
  176. outbuffer.append(buf)
  177. instream.stop_stream()
  178. outstream.stop_stream()
  179. instream.close()
  180. outstream.close()
  181. print('done')
  182. #print(repr(adcdiff))
  183. #print(repr(dacdiff))
  184. print(max(adcdacdiff), min(adcdacdiff))
  185. print(cnt)
  186. print(starttime)
  187. print(outbufferinfo[0][2])
  188. print(lasttime)
  189. print(outbufferinfo[-1][2])
  190. print(lasttime['current_time'] - starttime['current_time'])
  191. print('in status:', printstatus(combstatus))
  192. print('out status:', printstatus(functools.reduce(operator.__or__, (x[3] for x in outbufferinfo), 0)))
  193. ltimes = [ (e - s) * 1000 for s, e in times ]
  194. print(min(ltimes), max(ltimes))
  195. pa.terminate()