diff options
Diffstat (limited to 'client.py')
-rw-r--r-- | client.py | 239 |
1 files changed, 175 insertions, 64 deletions
@@ -13,6 +13,7 @@ import array import random import threading import thread +import colorsys from packet import Packet, CMD, stoi @@ -24,34 +25,58 @@ parser.add_option('-u', '--uid', dest='uid', default='', help='Set the UID (iden parser.add_option('-p', '--port', dest='port', type='int', default=13676, help='Set the port to listen on') parser.add_option('-r', '--rate', dest='rate', type='int', default=44100, help='Set the sample rate of the audio device') parser.add_option('-V', '--volume', dest='volume', type='float', default=1.0, help='Set the volume factor (>1 distorts, <1 attenuates)') +parser.add_option('-n', '--streams', dest='streams', type='int', default=1, help='Set the number of streams this client will play back') +parser.add_option('-N', '--numpy', dest='numpy', action='store_true', help='Use numpy acceleration') parser.add_option('-G', '--gui', dest='gui', default='', help='set a GUI to use') parser.add_option('--pg-fullscreen', dest='fullscreen', action='store_true', help='Use a full-screen video mode') parser.add_option('--pg-samp-width', dest='samp_width', type='int', help='Set the width of the sample pane (by default display width / 2)') parser.add_option('--pg-bgr-width', dest='bgr_width', type='int', help='Set the width of the bargraph pane (by default display width / 2)') parser.add_option('--pg-height', dest='height', type='int', help='Set the height of the window or full-screen video mode') +parser.add_option('--pg-no-colback', dest='no_colback', action='store_true', help='Don\'t render a colored background') +parser.add_option('--pg-low-freq', dest='low_freq', type='int', default=40, help='Low frequency for colored background') +parser.add_option('--pg-high-freq', dest='high_freq', type='int', default=1500, help='High frequency for colored background') +parser.add_option('--pg-log-base', dest='log_base', type='int', default=2, help='Logarithmic base for coloring (0 to make linear)') +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') options, args = parser.parse_args() +if options.numpy: + import numpy + PORT = options.port -STREAMS = 1 +STREAMS = options.streams IDENT = 'TONE' UID = options.uid -LAST_SAMP = 0 +LAST_SAMPS = [0] * STREAMS LAST_SAMPLES = [] -FREQ = 0 -PHASE = 0 +FREQS = [0] * STREAMS +PHASES = [0] * STREAMS RATE = options.rate FPB = 64 Z_SAMP = '\x00\x00\x00\x00' MAX = 0x7fffffff -AMP = MAX +AMPS = [MAX] * STREAMS MIN = -0x80000000 +EXPIRATIONS = [0] * STREAMS +QUEUED_PCM = '' + def lin_interp(frm, to, p): return p*to + (1-p)*frm +def rgb_for_freq_amp(f, a): + a = max((min((a, 1.0)), 0.0)) + pitchval = float(f - options.low_freq) / (options.high_freq - options.low_freq) + if options.log_base == 0: + try: + pitchval = math.log(pitchval) / math.log(options.log_base) + except ValueError: + pass + bgcol = colorsys.hls_to_rgb(min((1.0, max((0.0, pitchval)))), 0.5 * (a ** 2), 1.0) + return [int(i*255) for i in bgcol] + # GUIs GUIs = {} @@ -95,23 +120,42 @@ def pygame_notes(): PFAC = HEIGHT / 128.0 sampwin = pygame.Surface((SAMP_WIDTH, HEIGHT)) + sampwin.set_colorkey((0, 0, 0)) lastsy = HEIGHT / 2 + bgrwin = pygame.Surface((BGR_WIDTH, HEIGHT)) + bgrwin.set_colorkey((0, 0, 0)) clock = pygame.time.Clock() while True: - if FREQ > 0: - try: - pitch = 12 * math.log(FREQ / 440.0, 2) + 69 - except ValueError: - pitch = 0 + if options.no_colback: + disp.fill((0, 0, 0), (0, 0, WIDTH, HEIGHT)) else: - pitch = 0 - col = [int((AMP / MAX) * 255)] * 3 - - disp.fill((0, 0, 0), (BGR_WIDTH, 0, SAMP_WIDTH, HEIGHT)) - disp.scroll(-1, 0) - disp.fill(col, (BGR_WIDTH - 1, HEIGHT - pitch * PFAC - PFAC, 1, PFAC)) + gap = WIDTH / STREAMS + for i in xrange(STREAMS): + FREQ = FREQS[i] + AMP = AMPS[i] + if FREQ > 0: + bgcol = rgb_for_freq_amp(FREQ, float(AMP) / MAX) + else: + bgcol = (0, 0, 0) + #print i, ':', pitchval + disp.fill(bgcol, (i*gap, 0, gap, HEIGHT)) + + bgrwin.scroll(-1, 0) + bgrwin.fill((0, 0, 0), (BGR_WIDTH - 1, 0, 1, HEIGHT)) + for i in xrange(STREAMS): + FREQ = FREQS[i] + AMP = AMPS[i] + if FREQ > 0: + try: + pitch = 12 * math.log(FREQ / 440.0, 2) + 69 + except ValueError: + pitch = 0 + else: + pitch = 0 + col = [int((AMP / MAX) * 255)] * 3 + bgrwin.fill(col, (BGR_WIDTH - 1, HEIGHT - pitch * PFAC - PFAC, 1, PFAC)) sampwin.scroll(-len(LAST_SAMPLES), 0) x = max(0, SAMP_WIDTH - len(LAST_SAMPLES)) @@ -132,6 +176,7 @@ def pygame_notes(): # break #if len(pts) > 2: # pygame.gfxdraw.aapolygon(disp, pts, [0, 255, 0]) + disp.blit(bgrwin, (0, 0)) disp.blit(sampwin, (BGR_WIDTH, 0)) pygame.display.flip() @@ -272,45 +317,85 @@ if options.generators: #generator = square_wave generator = eval(options.generator) -def sigalrm(sig, frm): - global FREQ - FREQ = 0 - -def lin_seq(frm, to, cnt): - step = (to-frm)/float(cnt) - samps = [0]*cnt - for i in xrange(cnt): - p = i / float(cnt-1) - samps[i] = int(lin_interp(frm, to, p)) - return samps - -def samps(freq, phase, cnt): - global RATE, AMP - samps = [0]*cnt - for i in xrange(cnt): - samps[i] = int(AMP * max(-1, min(1, options.volume*generator((phase + 2 * math.pi * freq * i / RATE) % (2*math.pi))))) - return samps, (phase + 2 * math.pi * freq * cnt / RATE) % (2*math.pi) - -def to_data(samps): - return struct.pack('i'*len(samps), *samps) - -def gen_data(data, frames, time, status): - global FREQ, PHASE, Z_SAMP, LAST_SAMP, LAST_SAMPLES - if FREQ == 0: - PHASE = 0 - if LAST_SAMP == 0: - if options.gui: - LAST_SAMPLES.extend([0]*frames) - return (Z_SAMP*frames, pyaudio.paContinue) - fdata = lin_seq(LAST_SAMP, 0, frames) - if options.gui: - LAST_SAMPLES.extend(fdata) - LAST_SAMP = fdata[-1] - return (to_data(fdata), pyaudio.paContinue) - fdata, PHASE = samps(FREQ, PHASE, frames) +#def sigalrm(sig, frm): +# global FREQ +# FREQ = 0 + +if options.numpy: + def lin_seq(frm, to, cnt): + return numpy.linspace(frm, to, cnt, dtype=numpy.int32) + + def samps(freq, amp, phase, cnt): + samps = numpy.ndarray((cnt,), numpy.int32) + pvel = 2 * math.pi * freq / RATE + fac = options.volume * amp / float(STREAMS) + for i in xrange(cnt): + samps[i] = fac * max(-1, min(1, generator(phase))) + phase = (phase + pvel) % (2 * math.pi) + return samps, phase + + def to_data(samps): + return samps.tobytes() + + def mix(a, b): + return a + b + +else: + def lin_seq(frm, to, cnt): + step = (to-frm)/float(cnt) + samps = [0]*cnt + for i in xrange(cnt): + p = i / float(cnt-1) + samps[i] = int(lin_interp(frm, to, p)) + return samps + + def samps(freq, amp, phase, cnt): + global RATE + samps = [0]*cnt + for i in xrange(cnt): + samps[i] = int(2*amp / float(STREAMS) * max(-1, min(1, options.volume*generator((phase + 2 * math.pi * freq * i / RATE) % (2*math.pi))))) + return samps, (phase + 2 * math.pi * freq * cnt / RATE) % (2*math.pi) + + def to_data(samps): + return struct.pack('i'*len(samps), *samps) + + def mix(a, b): + return [min(MAX, max(MIN, i + j)) for i, j in zip(a, b)] + +def gen_data(data, frames, tm, status): + global FREQS, PHASE, Z_SAMP, LAST_SAMP, LAST_SAMPLES, QUEUED_PCM + if len(QUEUED_PCM) >= frames*4: + fdata = QUEUED_PCM[:frames*4] + QUEUED_PCM = QUEUED_PCM[frames*4:] + LAST_SAMPLES.extend(struct.unpack(str(frames)+'i', fdata)) + return fdata, pyaudio.paContinue + if options.numpy: + fdata = numpy.zeros((frames,), numpy.int32) + else: + fdata = [0] * frames + for i in range(STREAMS): + FREQ = FREQS[i] + LAST_SAMP = LAST_SAMPS[i] + AMP = AMPS[i] + EXPIRATION = EXPIRATIONS[i] + PHASE = PHASES[i] + if FREQ != 0: + if time.time() > EXPIRATION: + FREQ = 0 + FREQS[i] = 0 + if FREQ == 0: + PHASES[i] = 0 + if LAST_SAMP != 0: + vdata = lin_seq(LAST_SAMP, 0, frames) + fdata = mix(fdata, vdata) + LAST_SAMPS[i] = vdata[-1] + else: + vdata, PHASE = samps(FREQ, AMP, PHASE, frames) + fdata = mix(fdata, vdata) + PHASES[i] = PHASE + LAST_SAMPS[i] = vdata[-1] if options.gui: LAST_SAMPLES.extend(fdata) - LAST_SAMP = fdata[-1] return (to_data(fdata), pyaudio.paContinue) pa = pyaudio.PyAudio() @@ -322,21 +407,25 @@ if options.gui: guithread.start() if options.test: - FREQ = 440 + FREQS[0] = 440 + EXPIRATIONS[0] = time.time() + 1 time.sleep(1) - FREQ = 0 + FREQS[0] = 0 time.sleep(1) - FREQ = 880 + FREQS[0] = 880 + EXPIRATIONS[0] = time.time() + 1 time.sleep(1) - FREQ = 440 + FREQS[0] = 440 + EXPIRATIONS[0] = time.time() + 2 time.sleep(2) exit() sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(('', PORT)) -signal.signal(signal.SIGALRM, sigalrm) +#signal.signal(signal.SIGALRM, sigalrm) +counter = 0 while True: data = '' while not data: @@ -345,24 +434,46 @@ while True: except socket.error: pass pkt = Packet.FromStr(data) - print 'From', cli, 'command', pkt.cmd + crgb = [int(i*255) for i in colorsys.hls_to_rgb((float(counter) / options.counter_modulus) % 1.0, 0.5, 1.0)] + print '\x1b[38;2;{};{};{}m#'.format(*crgb), + counter += 1 + print '\x1b[mFrom', cli, 'command', pkt.cmd, if pkt.cmd == CMD.KA: - pass + print '\x1b[37mKA' elif pkt.cmd == CMD.PING: sock.sendto(data, cli) + print '\x1b[1;33mPING' elif pkt.cmd == CMD.QUIT: + print '\x1b[1;31mQUIT' break elif pkt.cmd == CMD.PLAY: + voice = pkt.data[4] dur = pkt.data[0]+pkt.data[1]/1000000.0 - FREQ = pkt.data[2] - AMP = MAX * (pkt.data[3]/255.0) - signal.setitimer(signal.ITIMER_REAL, dur) + FREQS[voice] = pkt.data[2] + AMPS[voice] = MAX * max(min(pkt.as_float(3), 1.0), 0.0) + EXPIRATIONS[voice] = time.time() + dur + vrgb = [int(i*255) for i in colorsys.hls_to_rgb(float(voice) / STREAMS * 2.0 / 3.0, 0.5, 1.0)] + frgb = rgb_for_freq_amp(pkt.data[2], pkt.as_float(3)) + print '\x1b[1;32mPLAY', + print '\x1b[1;38;2;{};{};{}mVOICE'.format(*vrgb), '{:03}'.format(voice), + print '\x1b[1;38;2;{};{};{}mFREQ'.format(*frgb), '{:04}'.format(pkt.data[2]), 'AMP', '%08.6f'%pkt.as_float(3), + if pkt.data[0] == 0 and pkt.data[1] == 0: + print '\x1b[1;35mSTOP!!!' + else: + print '\x1b[1;36mDUR', '%08.6f'%dur + #signal.setitimer(signal.ITIMER_REAL, dur) elif pkt.cmd == CMD.CAPS: data = [0] * 8 data[0] = STREAMS data[1] = stoi(IDENT) - for i in xrange(len(UID)/4): + for i in xrange(len(UID)/4 + 1): data[i+2] = stoi(UID[4*i:4*(i+1)]) sock.sendto(str(Packet(CMD.CAPS, *data)), cli) + print '\x1b[1;34mCAPS' + elif pkt.cmd == CMD.PCM: + fdata = data[4:] + fdata = struct.pack('16i', *[i<<16 for i in struct.unpack('16h', fdata)]) + QUEUED_PCM += fdata + print 'Now', len(QUEUED_PCM) / 4.0, 'frames queued' else: print 'Unknown cmd', pkt.cmd |