diff options
author | Graham Northup <grissess@nexusg.org> | 2019-11-16 00:26:03 -0500 |
---|---|---|
committer | Graham Northup <grissess@nexusg.org> | 2019-11-16 00:26:03 -0500 |
commit | da7b28f7c66ace58821a4f2add4007b373dcc8f9 (patch) | |
tree | bc9bb7569faa0d4decd8d77eebe385f8cd8a1be9 | |
parent | cf0b170b2cee2635905a087ea7ea9852512b5e98 (diff) |
Add separated renderer, update dissector
-rw-r--r-- | broadcast.py | 1 | ||||
-rw-r--r-- | client.py | 38 | ||||
-rw-r--r-- | dissector_itlc.lua | 26 | ||||
-rw-r--r-- | render.py | 144 |
4 files changed, 209 insertions, 0 deletions
diff --git a/broadcast.py b/broadcast.py index 7b9eb19..f7e3797 100644 --- a/broadcast.py +++ b/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) @@ -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'}, diff --git a/dissector_itlc.lua b/dissector_itlc.lua index eec5ceb..4431518 100644 --- a/dissector_itlc.lua +++ b/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()) diff --git a/render.py b/render.py new file mode 100644 index 0000000..882b85b --- /dev/null +++ b/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) |