text: #!/usr/bin/env python
  
  from ctypes import c_int, pointer, POINTER, sizeof, c_char, c_uint8, c_int16, cast
  from pyogg import opus
  from pyogg.opus import opus_int16, opus_int16_p, c_uchar
  
  import array
  import functools
  import operator
  import pyaudio
  import queue
  import sys
  import time
  import wave
  
  statusvals = [ 'paInputUnderflow', 'paInputOverflow', 'paOutputUnderflow', 'paOutputOverflow', 'paPrimingOutput' ]
  statuses = { getattr(pyaudio, y): y for y in statusvals }
  
  def printstatus(x):
  	r = []
  	while x:
  		b = x & -x	# get a single bit
  		r.append(statuses[b])
  		x &= ~b		# clear the bit we added
  
  	return ', '.join(r)
  
  rate = 48000
  framelength = .0025
  maxbw = 128*1024
  sampperframe = int(framelength * rate) # 5ms latency
  
  pa = pyaudio.PyAudio()
  
  for i in range(pa.get_device_count()):
  	print(i, repr(pa.get_device_info_by_index(i)))
  
  #sys.exit(0)
  class _OpusBase(object):
  	@staticmethod
  	def _check_err(err):
  		if err != opus.OPUS_OK:
  			raise RuntimeError('failed', err)
  
  class OpusDecoder(_OpusBase):
  	def __init__(self, rate, nchan):
  		err = c_int()
  		self._nchan = nchan
  		self._bytespersamp = nchan * sizeof(opus.opus_int16)
  		#print('foo:', repr((self._bytespersamp, nchan, sizeof(opus.opus_int16))))
  		self._pcmbuf = (opus_int16 * (nchan * int(rate * framelength)))()
  		self._dec = opus.opus_decoder_create(rate, nchan, pointer(err))
  		self._check_err(err.value)
  
  	def decode(self, frm):
  		#print(repr(frm))
  		r = opus.opus_decode(self._dec, cast(frm, POINTER(c_uchar)), len(frm), self._pcmbuf, len(self._pcmbuf), 0)
  		if r < 0:
  			self._check_err(r)
  
  		return array.array('h', self._pcmbuf[:self._nchan * r]).tobytes()
  
  	def __del__(self):
  		opus.opus_decoder_destroy(self._dec)
  		self._dec = None
  
  class OpusEncoder(_OpusBase):
  	def __init__(self, rate, nchan, app):
  		err = c_int()
  		self._nchan = nchan
  		self._bytespersamp = nchan * sizeof(opus.opus_int16)
  		#print('bar:', repr((self._bytespersamp, nchan, sizeof(opus.opus_int16))))
  		self._frbuf = (c_char * (self._bytespersamp * int(maxbw * framelength)))()
  		self._enc = opus.opus_encoder_create(rate, nchan, app, pointer(err))
  		self._check_err(err.value)
  
  	def get_max_bw(self):
  		val = opus.opus_int32()
  		r = opus.opus_encoder_ctl(self._enc, opus.OPUS_GET_MAX_BANDWIDTH, pointer(val))
  		self._check_err(r)
  		return val.value
  
  	def set_max_bw(self, maxbw):
  		r = opus.opus_encoder_ctl(self._enc, opus.OPUS_SET_MAX_BANDWIDTH, opus.opus_int32(maxbw))
  		self._check_err(r)
  
  	def get_inband_fec(self):
  		val = opus.opus_int32()
  		r = opus.opus_encoder_ctl(self._enc, opus.OPUS_GET_INBAND_FEC_REQUEST, pointer(val))
  		self._check_err(r)
  		return val.value
  
  	def set_inband_fec(self, maxbw):
  		r = opus.opus_encoder_ctl(self._enc, opus.OPUS_SET_INBAND_FEC_REQUEST, opus.opus_int32(maxbw))
  		self._check_err(r)
  
  	def get_pkt_loss(self):
  		val = opus.opus_int32()
  		r = opus.opus_encoder_ctl(self._enc, opus.OPUS_GET_PACKET_LOSS_PERC_REQUEST, pointer(val))
  		self._check_err(r)
  		return val.value
  
  	def set_pkt_loss(self, percent):
  		r = opus.opus_encoder_ctl(self._enc, opus.OPUS_SET_PACKET_LOSS_PERC_REQUEST, opus.opus_int32(percent))
  		self._check_err(r)
  
  	def encode(self, pcm):
  		fs = len(pcm) // self._bytespersamp
  		#print(repr(pcm), fs, repr(opus_int16_p), repr(self._nchan))
  		#print('baz:', fs, repr((len(pcm), self._bytespersamp)))
  		r = opus.opus_encode(self._enc, cast(pcm, opus_int16_p), fs, cast(self._frbuf, POINTER(c_uchar)), len(self._frbuf))
  		#r = opus.opus_encode(self._enc, pcm, fs, self._frbuf, len(self._frbuf))
  		if r < 0:
  			self._check_err(r)
  
  		#print(repr(self._frbuf), dir(self._frbuf))
  		return self._frbuf.raw[:r]
  
  	def __del__(self):
  		opus.opus_encoder_destroy(self._enc)
  		self._enc = None
  
  enc = OpusEncoder(rate, 1, opus.OPUS_APPLICATION_RESTRICTED_LOWDELAY)
  dec = OpusDecoder(rate, 1)
  
  enc.set_inband_fec(1)
  enc.set_pkt_loss(5)
  print('pl:', repr(enc.get_pkt_loss()))
  print('if:', repr(enc.get_inband_fec()))
  
  #sys.exit(0)
  
  inbuffer = queue.Queue()
  outbufferinfo = []
  outbuffer = []
  adcdiff = []
  dacdiff = []
  adcdacdiff = []
  
  times = []
  
  def excprinter(func):
  	@functools.wraps(func)
  	def func_wrapper(*args, **kwargs):
  		global times
  		try:
  			s = time.time()
  			r = func(*args, **kwargs)
  			times.append((s, time.time()))
  		except:
  			import traceback
  			traceback.print_exc()
  			raise
  
  		return r
  
  	return func_wrapper
  
  #wf = wave.open('foo.wav', 'wb')
  #wf.setnchannels(1)
  #wf.setsampwidth(2)
  #wf.setframerate(rate)
  
  cnt = 0
  starttime = None
  combstatus = 0
  
  @excprinter
  def incallback(in_data, frm_cnt, timeinfo, status):
  	inbuffer.put((in_data, frm_cnt, timeinfo, status))
  
  	if time.time() - s < 5:
  		return ('', pyaudio.paContinue)
  
  	inbuffer.put(None)	# signal finished
  	return ('', pyaudio.paComplete)
  
  def outcallback(in_data, frm_cnt, timeinfo, status):
  	outbufferinfo.append((in_data, frm_cnt, timeinfo, status))
  
  	if outbuffer:
  		buf = outbuffer.pop(0)
  	else:
  		buf = '\x00\x00' * sampperframe
  
  	if buf is None:
  		return (dbuf, pyaudio.paComplete)
  
  	return (buf, pyaudio.paContinue)
  
  instream = pa.open(rate=rate, channels=1, format=pyaudio.paInt16, input_device_index=0, input=True, frames_per_buffer=sampperframe, stream_callback=incallback)
  outstream = pa.open(rate=rate, channels=1, format=pyaudio.paInt16, output_device_index=3, output=True, frames_per_buffer=sampperframe, stream_callback=outcallback)
  print('il:', instream.get_input_latency())
  print('ol:', outstream.get_output_latency())
  
  s = time.time()
  
  print('starting')
  instream.start_stream()
  outstream.start_stream()
  while True:
  	#print('sleep')
  	d = inbuffer.get()
  	if d is None:
  		break
  	in_data, frm_cnt, timeinfo, status = d
  
  	combstatus |= status
  	if starttime is None:
  		starttime = timeinfo
  	lasttime = timeinfo
  
  	#print('pcb:', repr((type(in_data), len(in_data), frm_cnt, timeinfo, status)))
  	adcdiff.append(timeinfo['current_time'] - timeinfo['input_buffer_adc_time'])
  	dacdiff.append(timeinfo['output_buffer_dac_time'] - timeinfo['current_time'])
  	adcdacdiff.append(timeinfo['output_buffer_dac_time'] - timeinfo['input_buffer_adc_time'])
  	cnt += len(in_data)
  	#buf = enc.encode(in_data)
  	buf = in_data
  	#print('r:', len(buf), repr(buf), repr(type(buf)))
  	outbuffer.append(buf)
  
  instream.stop_stream()
  outstream.stop_stream()
  instream.close()
  outstream.close()
  print('done')
  
  #print(repr(adcdiff))
  #print(repr(dacdiff))
  print(max(adcdacdiff), min(adcdacdiff))
  print(cnt)
  print(starttime)
  print(outbufferinfo[0][2])
  print(lasttime)
  print(outbufferinfo[-1][2])
  print(lasttime['current_time'] - starttime['current_time'])
  print('in status:', printstatus(combstatus))
  print('out status:', printstatus(functools.reduce(operator.__or__, (x[3] for x in outbufferinfo), 0)))
  ltimes = [ (e - s) * 1000 for s, e in times ]
  print(min(ltimes), max(ltimes))
  pa.terminate()
  