Browse Source

Add separated renderer, update dissector

Graham Northup 6 years ago
parent
commit
da7b28f7c6
4 changed files with 209 additions and 0 deletions
  1. 1 0
      broadcast.py
  2. 38 0
      client.py
  3. 26 0
      dissector_itlc.lua
  4. 144 0
      render.py

+ 1 - 0
broadcast.py

@@ -211,6 +211,7 @@ except Exception:
     import traceback
     traceback.print_exc()
     rows, columns = 25, 80
+    print '---- Assuming default terminal size ----'
 
 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

+ 38 - 0
client.py

@@ -14,6 +14,9 @@ import random
 import threading
 import thread
 import colorsys
+import mmap
+import os
+import atexit
 
 from packet import Packet, CMD, PLF, stoi, OBLIGATE_POLYPHONE
 
@@ -43,6 +46,9 @@ parser.add_option('--pg-no-colback', dest='no_colback', action='store_true', hel
 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('--map-file', dest='map_file', default='client_map', help='File mapped by -G mapped (contains u32 frequency, f32 amplitude pairs for each voice)')
+parser.add_option('--map-interval', dest='map_interval', type='float', default=0.02, help='Period in seconds between refreshes of the map')
+parser.add_option('--map-samples', dest='map_samples', type='int', default=4096, help='Number of samples in the map file (MUST agree with renderer)')
 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')
 parser.add_option('--pcm-corr-rate', dest='pcm_corr_rate', type='float', default=0.05, help='Amount of time to correct buffer drift, measured as percentage of the current sync rate')
 
@@ -218,6 +224,38 @@ def pygame_notes():
 
         clock.tick(60)
 
+@GUI
+def mapped():
+    if os.path.exists(options.map_file):
+        raise ValueError('Refusing to map file--already exists!')
+    ms = options.map_samples
+    stm = options.map_interval
+    fixfmt = '>f'
+    fixfmtsz = struct.calcsize(fixfmt)
+    sigfmt = '>' + 'f' * ms
+    sigfmtsz = struct.calcsize(sigfmt)
+    strfmt = '>' + 'Lf' * STREAMS
+    strfmtsz = struct.calcsize(strfmt)
+    sz = sum((fixfmtsz, sigfmtsz, strfmtsz))
+    print 'Reserving', sz, 'in map file'
+    print 'Size triple:', fixfmtsz, sigfmtsz, strfmtsz
+    f = open(options.map_file, 'w+')
+    f.seek(sz - 1)
+    f.write('\0')
+    f.flush()
+    mapping = mmap.mmap(f.fileno(), sz, access=mmap.ACCESS_WRITE)
+    f.close()
+    atexit.register(os.unlink, options.map_file)
+    def unzip2(i):
+        for a, b in i:
+            yield a
+            yield b
+    while True:
+        mapping[:fixfmtsz] = struct.pack(fixfmt, (DRIFT_FACTOR - 1.0) if QUEUED_PCM else 0.0)
+        mapping[fixfmtsz:fixfmtsz+sigfmtsz] = struct.pack(sigfmt, *(float(LAST_SAMPLES[i])/MAX if i < len(LAST_SAMPLES) else 0.0 for i in xrange(ms)))
+        mapping[fixfmtsz+sigfmtsz:] = struct.pack(strfmt, *unzip2((FREQS[i], float(AMPS[i])/MAX) for i in xrange(STREAMS)))
+        time.sleep(stm)
+
 # Generator functions--should be cyclic within [0, 2*math.pi) and return [-1, 1]
 
 GENERATORS = [{'name': 'math.sin', 'args': None, 'desc': 'Sine function'},

+ 26 - 0
dissector_itlc.lua

@@ -13,6 +13,10 @@ local fields = {
 	ident = ProtoField.new("Client ID", "itlc.ident", ftypes.STRING),
 	pcm = ProtoField.new("PCM Data", "itlc.pcm", ftypes.INT16),
 	data = ProtoField.new("Unknown Data", "itlc.data", ftypes.BYTES),
+	buffered = ProtoField.new("Buffered Samples", "itlc.buffered", ftypes.UINT32),
+	artp = ProtoField.new("Articulation Parameter", "itlc.artp", ftypes.UINT32),
+	value = ProtoField.new("Value", "itlc.value", ftypes.FLOAT),
+	isglobal = ProtoField.new("Global?", "itlc.global", ftypes.BOOLEAN),
 }
 
 local fieldarray = {}
@@ -26,6 +30,8 @@ local commands = {
 	[3] = "PLAY",
 	[4] = "CAPS",
 	[5] = "PCM",
+	[6] = "PCMSYN",
+	[7] = "ARTP",
 }
 setmetatable(commands, {__index = function(self, k) return "(Unknown command!)" end})
 
@@ -60,6 +66,26 @@ local subdis = {
 	[5] = function(buffer, tree)
 		tree:add(fields.pcm, buffer())
 	end,
+	[6] = function(buffer, tree)
+		tree:add(fields.buffered, buffer(4, 4):uint())
+	end,
+	[7] = function(buffer, tree, pinfo)
+		local voice = buffer(4, 4):uint()
+		local glob = (voice == 0xffffffff)
+		tree:add(fields.port, voice)
+		tree:add(fields.isglobal, glob)
+		local artp = buffer(8, 4):uint()
+		local val = buffer(12, 4):float()
+		local fr = tree:add(fields.artp, artp)
+		if glob then
+			fr:append_text(" (Global)")
+			pinfo.cols.info = tostring(pinfo.cols.info) .. " GART(" .. artp .. ") = " .. val
+		else
+			fr:append_text(" (Local)")
+			pinfo.cols.info = tostring(pinfo.cols.info) .. " LART[" .. voice .. "](" .. artp .. ") = " .. val
+		end
+		tree:add(fields.value, val)
+	end,
 }
 setmetatable(subdis, {__index = function(self, k) return function(buffer, tree)
 	tree:add(fields.data, buffer())

+ 144 - 0
render.py

@@ -0,0 +1,144 @@
+# A visualizer for the Python client (or any other client) rendering to a mapped file
+
+import optparse
+import mmap
+import os
+import time
+import struct
+import colorsys
+import math
+
+import pygame
+import pygame.gfxdraw
+
+parser = optparse.OptionParser()
+parser.add_option('--map-file', dest='map_file', default='client_map', help='File mapped by -G mapped')
+parser.add_option('--map-samples', dest='map_samples', type='int', default=4096, help='Number of samples in the map file (MUST agree with client)')
+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-fullscreen', dest='fullscreen', action='store_true', help='Use a 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)')
+
+options, args = parser.parse_args()
+
+while not os.path.exists(options.map_file):
+    print 'Waiting for file to exist...'
+    time.sleep(1)
+
+f = open(options.map_file)
+mapping = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
+f.close()
+
+fixfmt = '>f'
+fixfmtsz = struct.calcsize(fixfmt)
+sigfmt = '>' + 'f' * options.map_samples
+sigfmtsz = struct.calcsize(sigfmt)
+strfmtsz = len(mapping) - fixfmtsz - sigfmtsz
+print 'Map size:', len(mapping), 'Appendix size:', strfmtsz
+print 'Size triple:', fixfmtsz, sigfmtsz, strfmtsz
+STREAMS = strfmtsz / struct.calcsize('>Lf')
+strfmt = '>' + 'Lf' * STREAMS
+print 'Detected', STREAMS, 'streams'
+
+pygame.init()
+
+WIDTH, HEIGHT = 640, 480
+dispinfo = pygame.display.Info()
+if dispinfo.current_h > 0 and dispinfo.current_w > 0:
+    WIDTH, HEIGHT = dispinfo.current_w, dispinfo.current_h
+
+flags = 0
+if options.fullscreen:
+    flags |= pygame.FULLSCREEN
+
+disp = pygame.display.set_mode((WIDTH, HEIGHT), flags)
+WIDTH, HEIGHT = disp.get_size()
+SAMP_WIDTH = WIDTH / 2
+if options.samp_width:
+    SAMP_WIDTH = options.samp_width
+BGR_WIDTH = WIDTH - SAMP_WIDTH
+HALFH = HEIGHT / 2
+PFAC = HEIGHT / 128.0
+sampwin = pygame.Surface((SAMP_WIDTH, HEIGHT))
+sampwin.set_colorkey((0, 0, 0))
+lastsy = HALFH
+bgrwin = pygame.Surface((BGR_WIDTH, HEIGHT))
+bgrwin.set_colorkey((0, 0, 0))
+
+clock = pygame.time.Clock()
+font = pygame.font.SysFont(pygame.font.get_default_font(), 24)
+
+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]
+
+while True:
+    DISP_FACTOR = struct.unpack(fixfmt, mapping[:fixfmtsz])[0]
+    LAST_SAMPLES = struct.unpack(sigfmt, mapping[fixfmtsz:fixfmtsz+sigfmtsz])
+    VALUES = struct.unpack(strfmt, mapping[fixfmtsz+sigfmtsz:])
+    FREQS, AMPS = VALUES[::2], VALUES[1::2]
+    if options.no_colback:
+        disp.fill((0, 0, 0), (0, 0, WIDTH, HEIGHT))
+    else:
+        gap = WIDTH / STREAMS
+        for i in xrange(STREAMS):
+            FREQ = FREQS[i]
+            AMP = AMPS[i]
+            if FREQ > 0:
+                bgcol = rgb_for_freq_amp(FREQ, AMP)
+            else:
+                bgcol = (0, 0, 0)
+            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 = [min(max(int(AMP * 255), 0), 255)] * 3
+        bgrwin.fill(col, (BGR_WIDTH - 1, HEIGHT - pitch * PFAC - PFAC, 1, PFAC))
+
+    sampwin.fill((0, 0, 0), (0, 0, SAMP_WIDTH, HEIGHT))
+    x = 0
+    for i in LAST_SAMPLES:
+        sy = int(AMP * HALFH + HALFH)
+        pygame.gfxdraw.line(sampwin, x - 1, lastsy, x, sy, (0, 255, 0))
+        x += 1
+        lastsy = sy
+
+    disp.blit(bgrwin, (0, 0))
+    disp.blit(sampwin, (BGR_WIDTH, 0))
+
+    if DISP_FACTOR != 0:
+        tsurf = font.render('%+011.6g'%(DISP_FACTOR,), True, (255, 255, 255), (0, 0, 0))
+        disp.fill((0, 0, 0), tsurf.get_rect())
+        disp.blit(tsurf, (0, 0))
+
+    pygame.display.flip()
+
+    for ev in pygame.event.get():
+        if ev.type == pygame.KEYDOWN:
+            if ev.key == pygame.K_ESCAPE:
+                pygame.quit()
+                exit()
+        elif ev.type == pygame.QUIT:
+            pygame.quit()
+            exit()
+
+    clock.tick(60)