Graham Northup 7 жил өмнө
parent
commit
c96689f87d
4 өөрчлөгдсөн 146 нэмэгдсэн , 32 устгасан
  1. 83 21
      broadcast.py
  2. 59 11
      client.py
  3. 3 0
      mkiv.py
  4. 1 0
      packet.py

+ 83 - 21
broadcast.py

@@ -46,15 +46,44 @@ parser.add_option('-n', '--number', dest='number', type='int', help='Number of c
 parser.add_option('--dry', dest='dry', action='store_true', help='Dry run--don\'t actually search for or play to clients, but pretend they exist (useful with -G)')
 parser.add_option('--dry', dest='dry', action='store_true', help='Dry run--don\'t actually search for or play to clients, but pretend they exist (useful with -G)')
 parser.add_option('--pcm', dest='pcm', action='store_true', help='Use experimental PCM rendering')
 parser.add_option('--pcm', dest='pcm', action='store_true', help='Use experimental PCM rendering')
 parser.add_option('--pcm-lead', dest='pcmlead', type='float', help='Seconds of leading PCM data to send')
 parser.add_option('--pcm-lead', dest='pcmlead', type='float', help='Seconds of leading PCM data to send')
+parser.add_option('--pcm-sync-every', dest='pcm_sync_every', type='int', help='How many PCM packets to wait before sending a SYNC event with buffer amounts')
 parser.add_option('--spin', dest='spin', action='store_true', help='Ignore delta times in the queue (busy loop the CPU) for higher accuracy')
 parser.add_option('--spin', dest='spin', action='store_true', help='Ignore delta times in the queue (busy loop the CPU) for higher accuracy')
+parser.add_option('--tapper', dest='tapper', type='float', help='When the main loop would wait this many seconds, wait instead for a keypress')
 parser.add_option('-G', '--gui', dest='gui', default='', help='set a GUI to use')
 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-fullscreen', dest='fullscreen', action='store_true', help='Use a full-screen video mode')
 parser.add_option('--pg-width', dest='pg_width', type='int', help='Width of the pygame window')
 parser.add_option('--pg-width', dest='pg_width', type='int', help='Width of the pygame window')
 parser.add_option('--pg-height', dest='pg_height', type='int', help='Width of the pygame window')
 parser.add_option('--pg-height', dest='pg_height', type='int', help='Width of the pygame window')
 parser.add_option('--help-routes', dest='help_routes', action='store_true', help='Show help about routing directives')
 parser.add_option('--help-routes', dest='help_routes', action='store_true', help='Show help about routing directives')
-parser.set_defaults(routes=['T:DRUM=!perc,0'], random=0.0, rand_low=80, rand_high=2000, live=None, factor=1.0, duration=0.25, volume=1.0, wait_time=0.1, tries=5, play=[], transpose=0, seek=0.0, bind_addr='', to=[], ports=[13676, 13677],  pg_width = 0, pg_height = 0, number=-1, pcmlead=0.1)
+parser.set_defaults(routes=['T:DRUM=!perc,0'], random=0.0, rand_low=80, rand_high=2000, live=None, factor=1.0, duration=0.25, volume=1.0, wait_time=0.1, tries=5, play=[], transpose=0, seek=0.0, bind_addr='', to=[], ports=[13676, 13677], tapper=None, pg_width = 0, pg_height = 0, number=-1, pcmlead=0.1, pcm_sync_every=4096)
 options, args = parser.parse_args()
 options, args = parser.parse_args()
 
 
+tap_func = None
+play_time = time.time
+if options.tapper is not None:
+    tap_play_time = 0.0
+    play_time = lambda: tap_play_time
+    if sys.platform.startswith('win'):
+        import msvcrt
+
+        tap_func = msvcrt.getch
+
+    else:
+        import termios, tty
+
+# https://stackoverflow.com/questions/1052107/reading-a-single-character-getch-style-in-python-is-not-working-in-unix
+        def unix_tap_func():
+            fd = sys.stdin.fileno()  # 0?
+            prev_settings = termios.tcgetattr(fd)
+            try:
+                mode = prev_settings[:]
+                mode[tty.LFLAG] &= ~(termios.ECHO | termios.ICANON)
+                termios.tcsetattr(fd, termios.TCSAFLUSH, mode)
+                return sys.stdin.read(1)
+            finally:
+                termios.tcsetattr(fd, termios.TCSADRAIN, prev_settings)
+
+        tap_func = unix_tap_func
+
 if options.help_routes:
 if options.help_routes:
     print '''Routes are a way of either exclusively or mutually binding certain streams to certain playback clients. They are especially fitting in heterogenous environments where some clients will outperform others in certain pitches or with certain parts.
     print '''Routes are a way of either exclusively or mutually binding certain streams to certain playback clients. They are especially fitting in heterogenous environments where some clients will outperform others in certain pitches or with certain parts.
 
 
@@ -77,9 +106,20 @@ The specifier consists of a comma-separated list of attribute-colon-value pairs,
     exit()
     exit()
 
 
 GUIS = {}
 GUIS = {}
-BASETIME = time.time()  # XXX fixes a race with the GUI
+BASETIME = play_time()  # XXX fixes a race with the GUI
 
 
 def gui_pygame():
 def gui_pygame():
+    # XXX Racy, do this fast
+    global tap_func
+    key_cond = threading.Condition()
+    if options.tapper is not None:
+
+        def pygame_tap_func():
+            with key_cond:
+                key_cond.wait()
+
+        tap_func = pygame_tap_func
+
     print 'Starting pygame GUI...'
     print 'Starting pygame GUI...'
     import pygame, colorsys
     import pygame, colorsys
     pygame.init()
     pygame.init()
@@ -125,13 +165,15 @@ def gui_pygame():
             col = [int(i*255) for i in col]
             col = [int(i*255) for i in col]
             disp.fill(col, (WIDTH - 1, HEIGHT - pitch * PFAC - PFAC, 1, PFAC))
             disp.fill(col, (WIDTH - 1, HEIGHT - pitch * PFAC - PFAC, 1, PFAC))
             idx += 1
             idx += 1
-        tsurf = font.render('%0.3f' % ((time.time() - BASETIME) / factor,), True, (255, 255, 255), (0, 0, 0))
+        tsurf = font.render('%0.3f' % ((play_time() - BASETIME) / factor,), True, (255, 255, 255), (0, 0, 0))
         disp.fill((0, 0, 0), tsurf.get_rect())
         disp.fill((0, 0, 0), tsurf.get_rect())
         disp.blit(tsurf, (0, 0))
         disp.blit(tsurf, (0, 0))
         pygame.display.flip()
         pygame.display.flip()
 
 
         for ev in pygame.event.get():
         for ev in pygame.event.get():
             if ev.type == pygame.KEYDOWN:
             if ev.type == pygame.KEYDOWN:
+                with key_cond:
+                    key_cond.notify()
                 if ev.key == pygame.K_ESCAPE:
                 if ev.key == pygame.K_ESCAPE:
                     thread.interrupt_main()
                     thread.interrupt_main()
                     pygame.quit()
                     pygame.quit()
@@ -387,17 +429,24 @@ for fname in args:
                 buf += nbuf
                 buf += nbuf
             return buf
             return buf
 
 
-        BASETIME = time.time() - options.pcmlead
+        BASETIME = play_time() - options.pcmlead
         sampcnt = 0
         sampcnt = 0
         buf = read_all(pcr, 32)
         buf = read_all(pcr, 32)
+        pcnt = 0
         print 'PCM: pcr', pcr, 'BASETIME', BASETIME, 'buf', len(buf)
         print 'PCM: pcr', pcr, 'BASETIME', BASETIME, 'buf', len(buf)
         while len(buf) >= 32:
         while len(buf) >= 32:
             frag = buf[:32]
             frag = buf[:32]
             buf = buf[32:]
             buf = buf[32:]
             for cl in clients:
             for cl in clients:
                 s.sendto(struct.pack('>L', CMD.PCM) + frag, cl)
                 s.sendto(struct.pack('>L', CMD.PCM) + frag, cl)
+            pcnt += 1
+            if pcnt >= options.pcm_sync_every:
+                for cl in clients:
+                    s.sendto(str(Packet(CMD.PCMSYN, int(options.pcmlead * samprate))), cl)
+                print 'PCMSYN'
+                pcnt = 0
             sampcnt += len(frag) / 2
             sampcnt += len(frag) / 2
-            delay = max(0, BASETIME + (sampcnt / float(samprate)) - time.time())
+            delay = max(0, BASETIME + (sampcnt / float(samprate)) - play_time())
             #print sampcnt, delay
             #print sampcnt, delay
             if delay > 0:
             if delay > 0:
                 time.sleep(delay)
                 time.sleep(delay)
@@ -547,28 +596,32 @@ for fname in args:
                 nsq, cls = self._Thread__args
                 nsq, cls = self._Thread__args
                 dur = None
                 dur = None
                 i = 0
                 i = 0
-                while nsq and float(nsq[0].get('time'))*factor <= time.time() - BASETIME:
+                while nsq and float(nsq[0].get('time'))*factor <= play_time() - BASETIME:
                     i += 1
                     i += 1
                     note = nsq.pop(0)
                     note = nsq.pop(0)
                     ttime = float(note.get('time'))
                     ttime = float(note.get('time'))
                     pitch = float(note.get('pitch')) + options.transpose
                     pitch = float(note.get('pitch')) + options.transpose
                     ampl = float(note.get('ampl', float(note.get('vel', 127.0)) / 127.0))
                     ampl = float(note.get('ampl', float(note.get('vel', 127.0)) / 127.0))
                     dur = factor*float(note.get('dur'))
                     dur = factor*float(note.get('dur'))
+                    pl_dur = dur if options.tapper is None else 65535
                     if options.verbose:
                     if options.verbose:
-                        print (time.time() - BASETIME) / options.factor, ': PLAY', pitch, dur, ampl
+                        print (play_time() - BASETIME) / options.factor, ': PLAY', pitch, dur, ampl
                     if options.dry:
                     if options.dry:
                         playing_notes[self.nsid] = (pitch, ampl)
                         playing_notes[self.nsid] = (pitch, ampl)
                     else:
                     else:
                         for cl in cls:
                         for cl in cls:
-                            s.sendto(str(Packet(CMD.PLAY, int(dur), int((dur*1000000)%1000000), int(440.0 * 2**((pitch-69)/12.0)), ampl * options.volume, cl[2])), cl[:2])
+                            s.sendto(str(Packet(CMD.PLAY, int(pl_dur), int((pl_dur*1000000)%1000000), int(440.0 * 2**((pitch-69)/12.0)), ampl * options.volume, cl[2])), cl[:2])
                             playing_notes[cl] = (pitch, ampl)
                             playing_notes[cl] = (pitch, ampl)
                 if i > 0 and dur is not None:
                 if i > 0 and dur is not None:
                     self.cur_offt = ttime + dur / options.factor
                     self.cur_offt = ttime + dur / options.factor
                 else:
                 else:
                     if self.cur_offt:
                     if self.cur_offt:
-                        if factor * self.cur_offt <= time.time() - BASETIME:
+                        if factor * self.cur_offt <= play_time() - BASETIME:
                             if options.verbose:
                             if options.verbose:
-                                print '% 6.5f'%((time.time() - BASETIME) / factor,), ': DONE'
+                                print '% 6.5f'%((play_time() - BASETIME) / factor,), ': DONE'
+                            if options.tapper is not None:
+                                for cl in cls:
+                                    s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0.0, cl[2])), cl[:2])
                             self.cur_offt = None
                             self.cur_offt = None
                             if options.dry:
                             if options.dry:
                                 playing_notes[self.nsid] = (0, 0)
                                 playing_notes[self.nsid] = (0, 0)
@@ -585,7 +638,7 @@ for fname in args:
             def drop_missed(self):
             def drop_missed(self):
                 nsq, cl = self._Thread__args
                 nsq, cl = self._Thread__args
                 cnt = 0
                 cnt = 0
-                while nsq and float(nsq[0].get('time'))*factor < time.time() - BASETIME:
+                while nsq and float(nsq[0].get('time'))*factor < play_time() - BASETIME:
                     nsq.pop(0)
                     nsq.pop(0)
                     cnt += 1
                     cnt += 1
                 if options.verbose:
                 if options.verbose:
@@ -601,20 +654,20 @@ for fname in args:
                             pitch = float(note.get('pitch')) + options.transpose
                             pitch = float(note.get('pitch')) + options.transpose
                             ampl = float(note.get('ampl', float(note.get('vel', 127.0)) / 127.0))
                             ampl = float(note.get('ampl', float(note.get('vel', 127.0)) / 127.0))
                             dur = factor*float(note.get('dur'))
                             dur = factor*float(note.get('dur'))
-                            while time.time() - BASETIME < factor*ttime:
-                                self.wait_for(factor*ttime - (time.time() - BASETIME))
+                            while play_time() - BASETIME < factor*ttime:
+                                self.wait_for(factor*ttime - (play_time() - BASETIME))
                             if options.dry:
                             if options.dry:
                                 cl = self.nsid  # XXX hack
                                 cl = self.nsid  # XXX hack
                             else:
                             else:
                                 for cl in cls:
                                 for cl in cls:
                                     s.sendto(str(Packet(CMD.PLAY, int(dur), int((dur*1000000)%1000000), int(440.0 * 2**((pitch-69)/12.0)), ampl * options.volume, cl[2])), cl[:2])
                                     s.sendto(str(Packet(CMD.PLAY, int(dur), int((dur*1000000)%1000000), int(440.0 * 2**((pitch-69)/12.0)), ampl * options.volume, cl[2])), cl[:2])
                             if options.verbose:
                             if options.verbose:
-                                print (time.time() - BASETIME), cl, ': PLAY', pitch, dur, vel
+                                print (play_time() - BASETIME), cl, ': PLAY', pitch, dur, vel
                             playing_notes[cl] = (pitch, ampl)
                             playing_notes[cl] = (pitch, ampl)
-                            self.wait_for(dur - ((time.time() - BASETIME) - factor*ttime))
+                            self.wait_for(dur - ((play_time() - BASETIME) - factor*ttime))
                             playing_notes[cl] = (0, 0)
                             playing_notes[cl] = (0, 0)
                     if options.verbose:
                     if options.verbose:
-                        print '% 6.5f'%(time.time() - BASETIME,), cl, ': DONE'
+                        print '% 6.5f'%(play_time() - BASETIME,), cl, ': DONE'
 
 
     threads = {}
     threads = {}
     if options.dry:
     if options.dry:
@@ -641,7 +694,7 @@ for fname in args:
         for thr in threads.values():
         for thr in threads.values():
             print thr._Thread__args[1]
             print thr._Thread__args[1]
 
 
-    BASETIME = time.time() - (options.seek*factor)
+    BASETIME = play_time() - (options.seek*factor)
     ENDTIME = max(max(float(n.get('time')) + float(n.get('dur')) for n in thr._Thread__args[0]) for thr in threads.values())
     ENDTIME = max(max(float(n.get('time')) + float(n.get('dur')) for n in thr._Thread__args[0]) for thr in threads.values())
     print 'Playtime is', ENDTIME
     print 'Playtime is', ENDTIME
     if options.seek > 0:
     if options.seek > 0:
@@ -651,9 +704,9 @@ for fname in args:
     SPINNERS = ['-', '\\', '|', '/']
     SPINNERS = ['-', '\\', '|', '/']
     while not all(thr.done for thr in threads.values()):
     while not all(thr.done for thr in threads.values()):
         for thr in threads.values():
         for thr in threads.values():
-            if thr.next_t is None or factor * thr.next_t <= time.time() - BASETIME:
+            if thr.next_t is None or factor * thr.next_t <= play_time() - BASETIME:
                 thr.actuate_missed()
                 thr.actuate_missed()
-        delta = factor * min(thr.next_t for thr in threads.values() if thr.next_t is not None) + BASETIME - time.time()
+        delta = factor * min(thr.next_t for thr in threads.values() if thr.next_t is not None) + BASETIME - play_time()
         if delta == float('inf'):
         if delta == float('inf'):
             print 'WARNING: Infinite postponement detected! Did all notestreams finish?'
             print 'WARNING: Infinite postponement detected! Did all notestreams finish?'
             break
             break
@@ -661,12 +714,21 @@ for fname in args:
             print 'TICK DELTA:', delta
             print 'TICK DELTA:', delta
         else:
         else:
             sys.stdout.write('\x1b[G\x1b[K[%s]' % (
             sys.stdout.write('\x1b[G\x1b[K[%s]' % (
-                ('#' * int((time.time() - BASETIME) * (columns - 2) / (ENDTIME * factor)) + SPINNERS[spin_phase]).ljust(columns - 2),
+                ('#' * int((play_time() - BASETIME) * (columns - 2) / (ENDTIME * factor)) + SPINNERS[spin_phase]).ljust(columns - 2),
             ))
             ))
             sys.stdout.flush()
             sys.stdout.flush()
             spin_phase += 1
             spin_phase += 1
             if spin_phase >= len(SPINNERS):
             if spin_phase >= len(SPINNERS):
                 spin_phase = 0
                 spin_phase = 0
         if delta >= 0 and not options.spin:
         if delta >= 0 and not options.spin:
-            time.sleep(delta)
+            if tap_func is not None:
+                if delta >= options.tapper:
+                    if options.verbose:
+                        print 'TAP'
+                    tap_func()
+                else:
+                    time.sleep(delta)
+                tap_play_time += delta
+            else:
+                time.sleep(delta)
     print fname, ': Done!'
     print fname, ': Done!'

+ 59 - 11
client.py

@@ -37,6 +37,7 @@ parser.add_option('--pg-low-freq', dest='low_freq', type='int', default=40, help
 parser.add_option('--pg-high-freq', dest='high_freq', type='int', default=1500, help='High 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('--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')
 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')
 
 
 options, args = parser.parse_args()
 options, args = parser.parse_args()
 
 
@@ -62,6 +63,9 @@ MIN = -0x80000000
 
 
 EXPIRATIONS = [0] * STREAMS
 EXPIRATIONS = [0] * STREAMS
 QUEUED_PCM = ''
 QUEUED_PCM = ''
+DRIFT_FACTOR = 1.0
+DRIFT_ERROR = 0.0
+LAST_SYN = None
 
 
 def lin_interp(frm, to, p):
 def lin_interp(frm, to, p):
     return p*to + (1-p)*frm
     return p*to + (1-p)*frm
@@ -340,6 +344,10 @@ if options.numpy:
     def mix(a, b):
     def mix(a, b):
         return a + b
         return a + b
 
 
+    def resample(samps, amt):
+        samps = numpy.frombuffer(samps, numpy.int32)
+        return numpy.interp(numpy.linspace(0, samps.shape[0], amt, False), numpy.linspace(0, samps.shape[0], samps.shape[0], False), samps).tobytes()
+
 else:
 else:
     def lin_seq(frm, to, cnt):
     def lin_seq(frm, to, cnt):
         step = (to-frm)/float(cnt)
         step = (to-frm)/float(cnt)
@@ -362,13 +370,37 @@ else:
     def mix(a, b):
     def mix(a, b):
         return [min(MAX, max(MIN, i + j)) for i, j in zip(a, b)]
         return [min(MAX, max(MIN, i + j)) for i, j in zip(a, b)]
 
 
+    def resample(samps, amt):
+        isl = len(samps) / 4
+        if isl == amt:
+            return samps
+        arr = struct.unpack(str(isl)+'i', samps)
+        out = []
+        for i in range(amt):
+            effidx = i * (isl / amt)
+            ieffidx = int(effidx)
+            if ieffidx == effidx:
+                out.append(arr[ieffidx])
+            else:
+                frac = effidx - ieffidx
+                out.append(arr[ieffidx] * (1-frac) + arr[ieffidx+1] * frac)
+        return struct.pack(str(amt)+'i', *out)
+
 def gen_data(data, frames, tm, status):
 def gen_data(data, frames, tm, status):
-    global FREQS, PHASE, Z_SAMP, LAST_SAMP, LAST_SAMPLES, QUEUED_PCM
+    global FREQS, PHASE, Z_SAMP, LAST_SAMP, LAST_SAMPLES, QUEUED_PCM, DRIFT_FACTOR, DRIFT_ERROR
     if len(QUEUED_PCM) >= frames*4:
     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
+        desired_frames = DRIFT_FACTOR * frames
+        err_frames = desired_frames - int(desired_frames)
+        desired_frames = int(desired_frames)
+        DRIFT_ERROR += err_frames
+        if DRIFT_ERROR >= 1.0:
+            desired_frames += 1
+            DRIFT_ERROR -= 1.0
+        fdata = QUEUED_PCM[:desired_frames*4]
+        QUEUED_PCM = QUEUED_PCM[desired_frames*4:]
+        if options.gui:
+            LAST_SAMPLES.extend(struct.unpack(str(desired_frames)+'i', fdata))
+        return resample(fdata, frames), pyaudio.paContinue
     if options.numpy:
     if options.numpy:
         fdata = numpy.zeros((frames,), numpy.int32)
         fdata = numpy.zeros((frames,), numpy.int32)
     else:
     else:
@@ -434,10 +466,11 @@ while True:
         except socket.error:
         except socket.error:
             pass
             pass
     pkt = Packet.FromStr(data)
     pkt = Packet.FromStr(data)
-    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.PCM:
+        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:
     if pkt.cmd == CMD.KA:
         print '\x1b[37mKA'
         print '\x1b[37mKA'
     elif pkt.cmd == CMD.PING:
     elif pkt.cmd == CMD.PING:
@@ -474,6 +507,21 @@ while True:
         fdata = data[4:]
         fdata = data[4:]
         fdata = struct.pack('16i', *[i<<16 for i in struct.unpack('16h', fdata)])
         fdata = struct.pack('16i', *[i<<16 for i in struct.unpack('16h', fdata)])
         QUEUED_PCM += fdata
         QUEUED_PCM += fdata
-        print 'Now', len(QUEUED_PCM) / 4.0, 'frames queued'
+        #print 'Now', len(QUEUED_PCM) / 4.0, 'frames queued'
+    elif pkt.cmd == CMD.PCMSYN:
+        print '\x1b[1;37mPCMSYN',
+        bufamt = pkt.data[0]
+        print '\x1b[0m DESBUF={}'.format(bufamt),
+        if LAST_SYN is None:
+            LAST_SYN = time.time()
+        else:
+            dt = time.time() - LAST_SYN
+            dfr = dt * RATE
+            bufnow = len(QUEUED_PCM) / 4
+            print '\x1b[35m CURBUF={}'.format(bufnow),
+            if bufnow != 0:
+                DRIFT_FACTOR = 1.0 + float(bufnow - bufamt) / (bufamt * dfr * options.pcm_corr_rate)
+                print '\x1b[37m (DRIFT_FACTOR=%08.6f)'%(DRIFT_FACTOR,),
+            print
     else:
     else:
-        print 'Unknown cmd', pkt.cmd
+        print '\x1b[1;31mUnknown cmd', pkt.cmd

+ 3 - 0
mkiv.py

@@ -754,6 +754,9 @@ for fname in args:
     ivargs = ET.SubElement(ivmeta, 'args')
     ivargs = ET.SubElement(ivmeta, 'args')
     ivargs.text = ' '.join('%r' % (i,) for i in sys.argv[1:])
     ivargs.text = ' '.join('%r' % (i,) for i in sys.argv[1:])
 
 
+    ivapp = ET.SubElement(ivmeta, 'app')
+    ivapp.text = 'mkiv'
+
     print 'Done.'
     print 'Done.'
     txt = ET.tostring(iv, 'UTF-8')
     txt = ET.tostring(iv, 'UTF-8')
     open(os.path.splitext(os.path.basename(fname))[0]+'.iv', 'wb').write(txt)
     open(os.path.splitext(os.path.basename(fname))[0]+'.iv', 'wb').write(txt)

+ 1 - 0
packet.py

@@ -25,6 +25,7 @@ class CMD:
 	PLAY = 3 # seconds, microseconds, frequency (Hz), amplitude (0.0 - 1.0), port
 	PLAY = 3 # seconds, microseconds, frequency (Hz), amplitude (0.0 - 1.0), port
         CAPS = 4 # ports, client type (1), user ident (2-7)
         CAPS = 4 # ports, client type (1), user ident (2-7)
         PCM = 5 # 16 samples, encoded S16_LE
         PCM = 5 # 16 samples, encoded S16_LE
+        PCMSYN = 6 # number of samples which should be buffered right now
 
 
 def itos(i):
 def itos(i):
     return struct.pack('>L', i).rstrip('\0')
     return struct.pack('>L', i).rstrip('\0')