client.py 14 KB

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