client.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. # A simple client that generates sine waves via python-pyaudio
  2. import signal
  3. import pyaudio
  4. import sys
  5. import socket
  6. import time
  7. import math
  8. import struct
  9. import socket
  10. import optparse
  11. import array
  12. import random
  13. import threading
  14. import thread
  15. import colorsys
  16. from packet import Packet, CMD, stoi
  17. parser = optparse.OptionParser()
  18. parser.add_option('-t', '--test', dest='test', action='store_true', help='Play a test sequence (440,<rest>,880,440), then exit')
  19. parser.add_option('-g', '--generator', dest='generator', default='math.sin', help='Set the generator (to a Python expression)')
  20. parser.add_option('--generators', dest='generators', action='store_true', help='Show the list of generators, then exit')
  21. parser.add_option('-u', '--uid', dest='uid', default='', help='Set the UID (identifier) of this client in the network')
  22. parser.add_option('-p', '--port', dest='port', type='int', default=13676, help='Set the port to listen on')
  23. parser.add_option('-r', '--rate', dest='rate', type='int', default=44100, help='Set the sample rate of the audio device')
  24. parser.add_option('-V', '--volume', dest='volume', type='float', default=1.0, help='Set the volume factor (>1 distorts, <1 attenuates)')
  25. parser.add_option('-n', '--streams', dest='streams', type='int', default=1, help='Set the number of streams this client will play back')
  26. parser.add_option('-N', '--numpy', dest='numpy', action='store_true', help='Use numpy acceleration')
  27. parser.add_option('-G', '--gui', dest='gui', default='', help='set a GUI to use')
  28. parser.add_option('--pg-fullscreen', dest='fullscreen', action='store_true', help='Use a full-screen video mode')
  29. parser.add_option('--pg-samp-width', dest='samp_width', type='int', help='Set the width of the sample pane (by default display width / 2)')
  30. parser.add_option('--pg-bgr-width', dest='bgr_width', type='int', help='Set the width of the bargraph pane (by default display width / 2)')
  31. parser.add_option('--pg-height', dest='height', type='int', help='Set the height of the window or full-screen video mode')
  32. parser.add_option('--pg-no-colback', dest='no_colback', action='store_true', help='Don\'t render a colored background')
  33. parser.add_option('--pg-low-freq', dest='low_freq', type='int', default=40, help='Low frequency for colored background')
  34. parser.add_option('--pg-high-freq', dest='high_freq', type='int', default=1500, help='High frequency for colored background')
  35. parser.add_option('--pg-log-base', dest='log_base', type='int', default=2, help='Logarithmic base for coloring (0 to make linear)')
  36. parser.add_option('--counter-modulus', dest='counter_modulus', type='int', default=16, help='Number of packet events in period of the terminal color scroll on the left margin')
  37. options, args = parser.parse_args()
  38. if options.numpy:
  39. import numpy
  40. PORT = options.port
  41. STREAMS = options.streams
  42. IDENT = 'TONE'
  43. UID = options.uid
  44. LAST_SAMPS = [0] * STREAMS
  45. LAST_SAMPLES = []
  46. FREQS = [0] * STREAMS
  47. PHASES = [0] * STREAMS
  48. RATE = options.rate
  49. FPB = 64
  50. Z_SAMP = '\x00\x00\x00\x00'
  51. MAX = 0x7fffffff
  52. AMPS = [MAX] * STREAMS
  53. MIN = -0x80000000
  54. EXPIRATIONS = [0] * STREAMS
  55. QUEUED_PCM = ''
  56. def lin_interp(frm, to, p):
  57. return p*to + (1-p)*frm
  58. def rgb_for_freq_amp(f, a):
  59. a = max((min((a, 1.0)), 0.0))
  60. pitchval = float(f - options.low_freq) / (options.high_freq - options.low_freq)
  61. if options.log_base == 0:
  62. try:
  63. pitchval = math.log(pitchval) / math.log(options.log_base)
  64. except ValueError:
  65. pass
  66. bgcol = colorsys.hls_to_rgb(min((1.0, max((0.0, pitchval)))), 0.5 * (a ** 2), 1.0)
  67. return [int(i*255) for i in bgcol]
  68. # GUIs
  69. GUIs = {}
  70. def GUI(f):
  71. GUIs[f.__name__] = f
  72. return f
  73. @GUI
  74. def pygame_notes():
  75. import pygame
  76. import pygame.gfxdraw
  77. pygame.init()
  78. dispinfo = pygame.display.Info()
  79. DISP_WIDTH = 640
  80. DISP_HEIGHT = 480
  81. if dispinfo.current_h > 0 and dispinfo.current_w > 0:
  82. DISP_WIDTH = dispinfo.current_w
  83. DISP_HEIGHT = dispinfo.current_h
  84. SAMP_WIDTH = DISP_WIDTH / 2
  85. if options.samp_width > 0:
  86. SAMP_WIDTH = options.samp_width
  87. BGR_WIDTH = DISP_WIDTH / 2
  88. if options.bgr_width > 0:
  89. BGR_WIDTH = options.bgr_width
  90. HEIGHT = DISP_HEIGHT
  91. if options.height > 0:
  92. HEIGHT = options.height
  93. flags = 0
  94. if options.fullscreen:
  95. flags |= pygame.FULLSCREEN
  96. disp = pygame.display.set_mode((SAMP_WIDTH + BGR_WIDTH, HEIGHT), flags)
  97. WIDTH, HEIGHT = disp.get_size()
  98. SAMP_WIDTH = WIDTH / 2
  99. BGR_WIDTH = WIDTH - SAMP_WIDTH
  100. PFAC = HEIGHT / 128.0
  101. sampwin = pygame.Surface((SAMP_WIDTH, HEIGHT))
  102. sampwin.set_colorkey((0, 0, 0))
  103. lastsy = HEIGHT / 2
  104. bgrwin = pygame.Surface((BGR_WIDTH, HEIGHT))
  105. bgrwin.set_colorkey((0, 0, 0))
  106. clock = pygame.time.Clock()
  107. while True:
  108. if options.no_colback:
  109. disp.fill((0, 0, 0), (0, 0, WIDTH, HEIGHT))
  110. else:
  111. gap = WIDTH / STREAMS
  112. for i in xrange(STREAMS):
  113. FREQ = FREQS[i]
  114. AMP = AMPS[i]
  115. if FREQ > 0:
  116. bgcol = rgb_for_freq_amp(FREQ, float(AMP) / MAX)
  117. else:
  118. bgcol = (0, 0, 0)
  119. #print i, ':', pitchval
  120. disp.fill(bgcol, (i*gap, 0, gap, HEIGHT))
  121. bgrwin.scroll(-1, 0)
  122. bgrwin.fill((0, 0, 0), (BGR_WIDTH - 1, 0, 1, HEIGHT))
  123. for i in xrange(STREAMS):
  124. FREQ = FREQS[i]
  125. AMP = AMPS[i]
  126. if FREQ > 0:
  127. try:
  128. pitch = 12 * math.log(FREQ / 440.0, 2) + 69
  129. except ValueError:
  130. pitch = 0
  131. else:
  132. pitch = 0
  133. col = [int((AMP / MAX) * 255)] * 3
  134. bgrwin.fill(col, (BGR_WIDTH - 1, HEIGHT - pitch * PFAC - PFAC, 1, PFAC))
  135. sampwin.scroll(-len(LAST_SAMPLES), 0)
  136. x = max(0, SAMP_WIDTH - len(LAST_SAMPLES))
  137. sampwin.fill((0, 0, 0), (x, 0, SAMP_WIDTH - x, HEIGHT))
  138. for i in LAST_SAMPLES:
  139. sy = int((float(i) / MAX) * (HEIGHT / 2) + (HEIGHT / 2))
  140. pygame.gfxdraw.line(sampwin, x - 1, lastsy, x, sy, (0, 255, 0))
  141. x += 1
  142. lastsy = sy
  143. del LAST_SAMPLES[:]
  144. #w, h = SAMP_WIDTH, HEIGHT
  145. #pts = [(BGR_WIDTH, HEIGHT / 2), (w + BGR_WIDTH, HEIGHT / 2)]
  146. #x = w + BGR_WIDTH
  147. #for i in reversed(LAST_SAMPLES):
  148. # pts.insert(1, (x, int((h / 2) + (float(i) / MAX) * (h / 2))))
  149. # x -= 1
  150. # if x < BGR_WIDTH:
  151. # break
  152. #if len(pts) > 2:
  153. # pygame.gfxdraw.aapolygon(disp, pts, [0, 255, 0])
  154. disp.blit(bgrwin, (0, 0))
  155. disp.blit(sampwin, (BGR_WIDTH, 0))
  156. pygame.display.flip()
  157. for ev in pygame.event.get():
  158. if ev.type == pygame.KEYDOWN:
  159. if ev.key == pygame.K_ESCAPE:
  160. thread.interrupt_main()
  161. pygame.quit()
  162. exit()
  163. elif ev.type == pygame.QUIT:
  164. thread.interrupt_main()
  165. pygame.quit()
  166. exit()
  167. clock.tick(60)
  168. # Generator functions--should be cyclic within [0, 2*math.pi) and return [-1, 1]
  169. GENERATORS = [{'name': 'math.sin', 'args': None, 'desc': 'Sine function'},
  170. {'name':'math.cos', 'args': None, 'desc': 'Cosine function'}]
  171. def generator(desc=None, args=None):
  172. def inner(f, desc=desc, args=args):
  173. if desc is None:
  174. desc = f.__doc__
  175. GENERATORS.append({'name': f.__name__, 'desc': desc, 'args': args})
  176. return f
  177. return inner
  178. @generator('Simple triangle wave (peaks/troughs at pi/2, 3pi/2)')
  179. def tri_wave(theta):
  180. if theta < math.pi/2:
  181. return lin_interp(0, 1, theta/(math.pi/2))
  182. elif theta < 3*math.pi/2:
  183. return lin_interp(1, -1, (theta-math.pi/2)/math.pi)
  184. else:
  185. return lin_interp(-1, 0, (theta-3*math.pi/2)/(math.pi/2))
  186. @generator('Saw wave (line from (0, 1) to (2pi, -1))')
  187. def saw_wave(theta):
  188. return lin_interp(1, -1, theta/(math.pi * 2))
  189. @generator('Simple square wave (piecewise 1 at x<pi, 0 else)')
  190. def square_wave(theta):
  191. if theta < math.pi:
  192. return 1
  193. else:
  194. return -1
  195. @generator('Random (noise) generator')
  196. def noise(theta):
  197. return random.random() * 2 - 1
  198. @generator('File generator', '(<file>[, <bits=8>[, <signed=True>[, <0=linear interp (default), 1=nearest>[, <swapbytes=False>]]]])')
  199. class file_samp(object):
  200. LINEAR = 0
  201. NEAREST = 1
  202. TYPES = {8: 'B', 16: 'H', 32: 'L'}
  203. def __init__(self, fname, bits=8, signed=True, samp=LINEAR, swab=False):
  204. tp = self.TYPES[bits]
  205. if signed:
  206. tp = tp.lower()
  207. self.max = float((2 << bits) - 1)
  208. self.buffer = array.array(tp)
  209. self.buffer.fromstring(open(fname, 'rb').read())
  210. if swab:
  211. self.buffer.byteswap()
  212. self.samp = samp
  213. def __call__(self, theta):
  214. norm = theta / (2*math.pi)
  215. if self.samp == self.LINEAR:
  216. v = norm*len(self.buffer)
  217. l = int(math.floor(v))
  218. h = int(math.ceil(v))
  219. if l == h:
  220. return self.buffer[l]/self.max
  221. if h >= len(self.buffer):
  222. h = 0
  223. return lin_interp(self.buffer[l], self.buffer[h], v-l)/self.max
  224. elif self.samp == self.NEAREST:
  225. return self.buffer[int(math.ceil(norm*len(self.buffer) - 0.5))]/self.max
  226. @generator('Harmonics generator (adds overtones at f, 2f, 3f, 4f, etc.)', '(<generator>, <amplitude of f>, <amp 2f>, <amp 3f>, ...)')
  227. class harmonic(object):
  228. def __init__(self, gen, *spectrum):
  229. self.gen = gen
  230. self.spectrum = spectrum
  231. def __call__(self, theta):
  232. return max(-1, min(1, sum([amp*self.gen((i+1)*theta % (2*math.pi)) for i, amp in enumerate(self.spectrum)])))
  233. @generator('General harmonics generator (adds arbitrary overtones)', '(<generator>, <factor of f>, <amplitude>, <factor>, <amplitude>, ...)')
  234. class genharmonic(object):
  235. def __init__(self, gen, *harmonics):
  236. self.gen = gen
  237. self.harmonics = zip(harmonics[::2], harmonics[1::2])
  238. def __call__(self, theta):
  239. return max(-1, min(1, sum([amp * self.gen(i * theta % (2*math.pi)) for i, amp in self.harmonics])))
  240. @generator('Mix generator', '(<generator>[, <amp>], [<generator>[, <amp>], [...]])')
  241. class mixer(object):
  242. def __init__(self, *specs):
  243. self.pairs = []
  244. i = 0
  245. while i < len(specs):
  246. if i+1 < len(specs) and isinstance(specs[i+1], (float, int)):
  247. pair = (specs[i], specs[i+1])
  248. i += 2
  249. else:
  250. pair = (specs[i], None)
  251. i += 1
  252. self.pairs.append(pair)
  253. tamp = 1 - min(1, sum([amp for gen, amp in self.pairs if amp is not None]))
  254. parts = float(len([None for gen, amp in self.pairs if amp is None]))
  255. for idx, pair in enumerate(self.pairs):
  256. if pair[1] is None:
  257. self.pairs[idx] = (pair[0], tamp / parts)
  258. def __call__(self, theta):
  259. return max(-1, min(1, sum([amp*gen(theta) for gen, amp in self.pairs])))
  260. @generator('Phase offset generator (in radians; use math.pi)', '(<generator>, <offset>)')
  261. class phase_off(object):
  262. def __init__(self, gen, offset):
  263. self.gen = gen
  264. self.offset = offset
  265. def __call__(self, theta):
  266. return self.gen((theta + self.offset) % (2*math.pi))
  267. if options.generators:
  268. for item in GENERATORS:
  269. print item['name'],
  270. if item['args'] is not None:
  271. print item['args'],
  272. print '--', item['desc']
  273. exit()
  274. #generator = math.sin
  275. #generator = tri_wave
  276. #generator = square_wave
  277. generator = eval(options.generator)
  278. #def sigalrm(sig, frm):
  279. # global FREQ
  280. # FREQ = 0
  281. if options.numpy:
  282. def lin_seq(frm, to, cnt):
  283. return numpy.linspace(frm, to, cnt, dtype=numpy.int32)
  284. def samps(freq, amp, phase, cnt):
  285. samps = numpy.ndarray((cnt,), numpy.int32)
  286. pvel = 2 * math.pi * freq / RATE
  287. fac = options.volume * amp / float(STREAMS)
  288. for i in xrange(cnt):
  289. samps[i] = fac * max(-1, min(1, generator(phase)))
  290. phase = (phase + pvel) % (2 * math.pi)
  291. return samps, phase
  292. def to_data(samps):
  293. return samps.tobytes()
  294. def mix(a, b):
  295. return a + b
  296. else:
  297. def lin_seq(frm, to, cnt):
  298. step = (to-frm)/float(cnt)
  299. samps = [0]*cnt
  300. for i in xrange(cnt):
  301. p = i / float(cnt-1)
  302. samps[i] = int(lin_interp(frm, to, p))
  303. return samps
  304. def samps(freq, amp, phase, cnt):
  305. global RATE
  306. samps = [0]*cnt
  307. for i in xrange(cnt):
  308. samps[i] = int(2*amp / float(STREAMS) * max(-1, min(1, options.volume*generator((phase + 2 * math.pi * freq * i / RATE) % (2*math.pi)))))
  309. return samps, (phase + 2 * math.pi * freq * cnt / RATE) % (2*math.pi)
  310. def to_data(samps):
  311. return struct.pack('i'*len(samps), *samps)
  312. def mix(a, b):
  313. return [min(MAX, max(MIN, i + j)) for i, j in zip(a, b)]
  314. def gen_data(data, frames, tm, status):
  315. global FREQS, PHASE, Z_SAMP, LAST_SAMP, LAST_SAMPLES, QUEUED_PCM
  316. if len(QUEUED_PCM) >= frames*4:
  317. fdata = QUEUED_PCM[:frames*4]
  318. QUEUED_PCM = QUEUED_PCM[frames*4:]
  319. LAST_SAMPLES.extend(struct.unpack(str(frames)+'i', fdata))
  320. return fdata, pyaudio.paContinue
  321. if options.numpy:
  322. fdata = numpy.zeros((frames,), numpy.int32)
  323. else:
  324. fdata = [0] * frames
  325. for i in range(STREAMS):
  326. FREQ = FREQS[i]
  327. LAST_SAMP = LAST_SAMPS[i]
  328. AMP = AMPS[i]
  329. EXPIRATION = EXPIRATIONS[i]
  330. PHASE = PHASES[i]
  331. if FREQ != 0:
  332. if time.time() > EXPIRATION:
  333. FREQ = 0
  334. FREQS[i] = 0
  335. if FREQ == 0:
  336. PHASES[i] = 0
  337. if LAST_SAMP != 0:
  338. vdata = lin_seq(LAST_SAMP, 0, frames)
  339. fdata = mix(fdata, vdata)
  340. LAST_SAMPS[i] = vdata[-1]
  341. else:
  342. vdata, PHASE = samps(FREQ, AMP, PHASE, frames)
  343. fdata = mix(fdata, vdata)
  344. PHASES[i] = PHASE
  345. LAST_SAMPS[i] = vdata[-1]
  346. if options.gui:
  347. LAST_SAMPLES.extend(fdata)
  348. return (to_data(fdata), pyaudio.paContinue)
  349. pa = pyaudio.PyAudio()
  350. stream = pa.open(rate=RATE, channels=1, format=pyaudio.paInt32, output=True, frames_per_buffer=FPB, stream_callback=gen_data)
  351. if options.gui:
  352. guithread = threading.Thread(target=GUIs[options.gui])
  353. guithread.setDaemon(True)
  354. guithread.start()
  355. if options.test:
  356. FREQS[0] = 440
  357. EXPIRATIONS[0] = time.time() + 1
  358. time.sleep(1)
  359. FREQS[0] = 0
  360. time.sleep(1)
  361. FREQS[0] = 880
  362. EXPIRATIONS[0] = time.time() + 1
  363. time.sleep(1)
  364. FREQS[0] = 440
  365. EXPIRATIONS[0] = time.time() + 2
  366. time.sleep(2)
  367. exit()
  368. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  369. sock.bind(('', PORT))
  370. #signal.signal(signal.SIGALRM, sigalrm)
  371. counter = 0
  372. while True:
  373. data = ''
  374. while not data:
  375. try:
  376. data, cli = sock.recvfrom(4096)
  377. except socket.error:
  378. pass
  379. pkt = Packet.FromStr(data)
  380. crgb = [int(i*255) for i in colorsys.hls_to_rgb((float(counter) / options.counter_modulus) % 1.0, 0.5, 1.0)]
  381. print '\x1b[38;2;{};{};{}m#'.format(*crgb),
  382. counter += 1
  383. print '\x1b[mFrom', cli, 'command', pkt.cmd,
  384. if pkt.cmd == CMD.KA:
  385. print '\x1b[37mKA'
  386. elif pkt.cmd == CMD.PING:
  387. sock.sendto(data, cli)
  388. print '\x1b[1;33mPING'
  389. elif pkt.cmd == CMD.QUIT:
  390. print '\x1b[1;31mQUIT'
  391. break
  392. elif pkt.cmd == CMD.PLAY:
  393. voice = pkt.data[4]
  394. dur = pkt.data[0]+pkt.data[1]/1000000.0
  395. FREQS[voice] = pkt.data[2]
  396. AMPS[voice] = MAX * max(min(pkt.as_float(3), 1.0), 0.0)
  397. EXPIRATIONS[voice] = time.time() + dur
  398. vrgb = [int(i*255) for i in colorsys.hls_to_rgb(float(voice) / STREAMS * 2.0 / 3.0, 0.5, 1.0)]
  399. frgb = rgb_for_freq_amp(pkt.data[2], pkt.as_float(3))
  400. print '\x1b[1;32mPLAY',
  401. print '\x1b[1;38;2;{};{};{}mVOICE'.format(*vrgb), '{:03}'.format(voice),
  402. print '\x1b[1;38;2;{};{};{}mFREQ'.format(*frgb), '{:04}'.format(pkt.data[2]), 'AMP', '%08.6f'%pkt.as_float(3),
  403. if pkt.data[0] == 0 and pkt.data[1] == 0:
  404. print '\x1b[1;35mSTOP!!!'
  405. else:
  406. print '\x1b[1;36mDUR', '%08.6f'%dur
  407. #signal.setitimer(signal.ITIMER_REAL, dur)
  408. elif pkt.cmd == CMD.CAPS:
  409. data = [0] * 8
  410. data[0] = STREAMS
  411. data[1] = stoi(IDENT)
  412. for i in xrange(len(UID)/4 + 1):
  413. data[i+2] = stoi(UID[4*i:4*(i+1)])
  414. sock.sendto(str(Packet(CMD.CAPS, *data)), cli)
  415. print '\x1b[1;34mCAPS'
  416. elif pkt.cmd == CMD.PCM:
  417. fdata = data[4:]
  418. fdata = struct.pack('16i', *[i<<16 for i in struct.unpack('16h', fdata)])
  419. QUEUED_PCM += fdata
  420. print 'Now', len(QUEUED_PCM) / 4.0, 'frames queued'
  421. else:
  422. print 'Unknown cmd', pkt.cmd