aboutsummaryrefslogtreecommitdiff
path: root/client.py
diff options
context:
space:
mode:
Diffstat (limited to 'client.py')
-rw-r--r--client.py239
1 files changed, 175 insertions, 64 deletions
diff --git a/client.py b/client.py
index 2d1ab40..2fcaae9 100644
--- a/client.py
+++ b/client.py
@@ -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