Sfoglia il codice sorgente

Rendering updates, minor bugfixes

Grissess 9 anni fa
parent
commit
712ca8f06e
4 ha cambiato i file con 108 aggiunte e 51 eliminazioni
  1. 60 31
      broadcast.py
  2. 33 4
      client.py
  3. 7 3
      drums.py
  4. 8 13
      mkiv.py

+ 60 - 31
broadcast.py

@@ -8,14 +8,15 @@ import thread
 import optparse
 import random
 import itertools
+import re
 
 from packet import Packet, CMD, itos, OBLIGATE_POLYPHONE
 
 parser = optparse.OptionParser()
 parser.add_option('-t', '--test', dest='test', action='store_true', help='Play a test tone (440, 880) on all clients in sequence (the last overlaps with the first of the next)')
-parser.add_option('--test-delay', dest='test_delay', type='float', help='Time for which to play a test tone')
 parser.add_option('-T', '--transpose', dest='transpose', type='int', help='Transpose by a set amount of semitones (positive or negative)')
 parser.add_option('--sync-test', dest='sync_test', action='store_true', help='Don\'t wait for clients to play tones properly--have them all test tone at the same time')
+parser.add_option('--wait-test', dest='wait_test', action='store_true', help='Wait for user input before moving to the next client tested')
 parser.add_option('-R', '--random', dest='random', type='float', help='Generate random notes at approximately this period')
 parser.add_option('--rand-low', dest='rand_low', type='int', help='Low frequency to randomly sample')
 parser.add_option('--rand-high', dest='rand_high', type='int', help='High frequency to randomly sample')
@@ -48,7 +49,7 @@ parser.add_option('--pg-fullscreen', dest='fullscreen', action='store_true', hel
 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('--help-routes', dest='help_routes', action='store_true', help='Show help about routing directives')
-parser.set_defaults(routes=[], test_delay=0.25, random=0.0, rand_low=80, rand_high=2000, live=None, factor=1.0, duration=1.0, volume=1.0, wait_time=0.1, tries=5, play=[], transpose=0, seek=0.0, bind_addr='', ports=[13676],  pg_width = 0, pg_height = 0, number=-1, pcmlead=0.1)
+parser.set_defaults(routes=[], 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='', ports=[13676],  pg_width = 0, pg_height = 0, number=-1, pcmlead=0.1)
 options, args = parser.parse_args()
 
 if options.help_routes:
@@ -57,8 +58,13 @@ if options.help_routes:
 Routes are fully specified by:
 -The attribute to be routed on (either type "T", or UID "U")
 -The value of that attribute
--The exclusivity of that route ("+" for inclusive, "-" for exclusive)
--The stream group to be routed there.
+-The exclusivity of that route ("+" for inclusive, "-" for exclusive, "!" for complete)
+-The stream group to be routed there, or 0 to null route.
+The first two may be replaced by a single '0' to null route a stream--effective only when used with an exclusive route.
+
+"Complete" exclusivity is valid only for obligate polyphones, and indicates that *all* matches are to receive the stream. In other cases, this will have the undesirable effect of routing only one stream.
+
+The special group ALL matches all streams. Regular expressions may be used to specify groups. Note that the first character is *not* part of the regular expression.
 
 The syntax for that specification resembles the following:
 
@@ -68,6 +74,7 @@ The specifier consists of a comma-separated list of attribute-colon-value pairs,
     exit()
 
 GUIS = {}
+BASETIME = time.time()  # XXX fixes a race with the GUI
 
 def gui_pygame():
     print 'Starting pygame GUI...'
@@ -181,15 +188,21 @@ for num in xrange(options.tries):
         uid_groups.setdefault(uid, set()).add(cl)
         type_groups.setdefault(tp, set()).add(cl)
 	if options.test:
-                ts, tms = int(options.test_delay), int(options.test_delay * 1000000) % 1000000
-		s.sendto(str(Packet(CMD.PLAY, ts, tms, 440, options.volume)), cl)
+            ts, tms = int(options.duration), int(options.duration * 1000000) % 1000000
+            if options.wait_test:
+                s.sendto(str(Packet(CMD.PLAY, 65535, 0, 440, options.volume)), cl)
+                raw_input('%r: Press enter to test next client...' %(cl,))
+                s.sendto(str(Packet(CMD.PLAY, ts, tms, 880, options.volume)), cl)
+            else:
+                s.sendto(str(Packet(CMD.PLAY, ts, tms, 440, options.volume)), cl)
                 if not options.sync_test:
-                    time.sleep(options.test_delay)
+                    time.sleep(options.duration)
                     s.sendto(str(Packet(CMD.PLAY, ts, tms, 880, options.volume)), cl)
 	if options.quit:
 		s.sendto(str(Packet(CMD.QUIT)), cl)
         if options.silence:
-                s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0.0)), cl)
+            for i in xrange(pkt.data[0]):
+                s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0.0, i)), cl)
         if pkt.data[0] == OBLIGATE_POLYPHONE:
             pkt.data[0] = 1
         for i in xrange(pkt.data[0]):
@@ -219,7 +232,7 @@ if options.play:
 if options.test and options.sync_test:
     time.sleep(0.25)
     for cl in targets:
-        s.sendto(str(Packet(CMD.PLAY, 0, 250000, 880, 1.0, cl[2])), cl[:2])
+        s.sendto(str(Packet(CMD.PLAY, 0, 250000, 880, options.volume, cl[2])), cl[:2])
 
 if options.test or options.quit or options.silence:
     print uid_groups
@@ -330,6 +343,7 @@ if options.repeat:
 
 for fname in args:
     if options.pcm and not fname.endswith('.iv'):
+        print 'PCM: play', fname
         if fname == '-':
             import wave
             pcr = wave.open(sys.stdin)
@@ -359,7 +373,8 @@ for fname in args:
 
         BASETIME = time.time() - options.pcmlead
         sampcnt = 0
-        buf = read_all(pcr, 16)
+        buf = read_all(pcr, 32)
+        print 'PCM: pcr', pcr, 'BASETIME', BASETIME, 'buf', len(buf)
         while len(buf) >= 32:
             frag = buf[:32]
             buf = buf[32:]
@@ -371,7 +386,9 @@ for fname in args:
             if delay > 0:
                 time.sleep(delay)
             if len(buf) < 32:
-                buf += read_all(pcr, 16)
+                buf += read_all(pcr, 32 - len(buf))
+        print 'PCM: exit'
+        continue
     try:
         iv = ET.parse(fname).getroot()
     except IOError:
@@ -390,7 +407,7 @@ for fname in args:
     print number, 'clients used (number)'
 
     class Route(object):
-        def __init__(self, fattr, fvalue, group, excl=False):
+        def __init__(self, fattr, fvalue, group, excl=False, complete=False):
             if fattr == 'U':
                 self.map = uid_groups
             elif fattr == 'T':
@@ -400,10 +417,9 @@ for fname in args:
             else:
                 raise ValueError('Not a valid attribute specifier: %r'%(fattr,))
             self.value = fvalue
-            if group is not None and group not in groups:
-                raise ValueError('Not a present group: %r'%(group,))
             self.group = group
             self.excl = excl
+            self.complete = complete
         @classmethod
         def Parse(cls, s):
             fspecs, _, grpspecs = map(lambda x: x.strip(), s.partition('='))
@@ -418,6 +434,8 @@ for fname in args:
                         ret.append(Route(fattr, fvalue, part[1:], False))
                     elif part[0] == '-':
                         ret.append(Route(fattr, fvalue, part[1:], True))
+                    elif part[0] == '!':
+                        ret.append(Route(fattr, fvalue, part[1:], True, True))
                     elif part[0] == '0':
                         ret.append(Route(fattr, fvalue, None, True))
                     else:
@@ -432,26 +450,35 @@ for fname in args:
         def __init__(self, clis=None):
             if clis is None:
                 clis = set(targets)
-            self.clients = clis
+            self.clients = list(clis)
             self.routes = []
         def Route(self, stream):
-            testset = set(self.clients)
+            testset = self.clients
             grp = stream.get('group', 'ALL')
             if options.verbose:
                 print 'Routing', grp, '...'
             excl = False
             for route in self.routes:
-                if route.group == grp:
+                if route.group is not None and re.match(route.group, grp) is not None:
                     if options.verbose:
                         print '\tMatches route', route
                     excl = excl or route.excl
                     matches = filter(lambda x, route=route: route.Apply(x), testset)
                     if matches:
+                        if route.complete:
+                            if options.verbose:
+                                print '\tUsing ALL clients:', matches
+                            for cl in matches:
+                                self.clients.remove(matches[0])
+                                if ports.get(matches[0][:2]) == OBLIGATE_POLYPHONE:
+                                    self.clients.append(matches[0])
+                            return matches
                         if options.verbose:
                             print '\tUsing client', matches[0]
-                        if ports.get(matches[0][:2]) != OBLIGATE_POLYPHONE:
-                            self.clients.remove(matches[0])
-                        return matches[0]
+                        self.clients.remove(matches[0])
+                        if ports.get(matches[0][:2]) == OBLIGATE_POLYPHONE:
+                            self.clients.append(matches[0])
+                        return [matches[0]]
                     if options.verbose:
                         print '\tNo matches, moving on...'
                 if route.group is None:
@@ -468,17 +495,18 @@ for fname in args:
             if excl:
                 if options.verbose:
                     print '\tExclusively routed, no route matched.'
-                return None
+                return []
             if not testset:
                 if options.verbose:
                     print '\tOut of clients, no route matched.'
-                return None
+                return []
             cli = list(testset)[0]
-            if ports.get(cli[:2]) != OBLIGATE_POLYPHONE:
-                self.clients.remove(cli)
+            self.clients.remove(cli)
+            if ports.get(cli[:2]) == OBLIGATE_POLYPHONE:
+                self.clients.append(cli)
             if options.verbose:
                 print '\tDefault route to', cli
-            return cli
+            return [cli]
 
     routeset = RouteSet()
     for rspec in options.routes:
@@ -513,7 +541,7 @@ for fname in args:
                     if options.verbose:
                         print (time.time() - BASETIME) / options.factor, ': PLAY', pitch, dur, ampl
                     if options.dry:
-                        playing_notes[self.ident] = (pitch, ampl)
+                        playing_notes[self.nsid] = (pitch, ampl)
                     else:
                         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])
@@ -527,7 +555,7 @@ for fname in args:
                                 print '% 6.5f'%((time.time() - BASETIME) / factor,), ': DONE'
                             self.cur_offt = None
                             if options.dry:
-                                playing_notes[self.ident] = (0, 0)
+                                playing_notes[self.nsid] = (0, 0)
                             else:
                                 for cl in cls:
                                     playing_notes[cl] = (0, 0)
@@ -560,7 +588,7 @@ for fname in args:
                             while time.time() - BASETIME < factor*ttime:
                                 self.wait_for(factor*ttime - (time.time() - BASETIME))
                             if options.dry:
-                                cl = self.ident  # XXX hack
+                                cl = self.nsid  # XXX hack
                             else:
                                 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])
@@ -574,16 +602,17 @@ for fname in args:
 
     threads = {}
     if options.dry:
-        for ns in notestreams:
+        for nsid, ns in enumerate(notestreams):
             nsq = ns.findall('note')
             nsq.sort(key=lambda x: float(x.get('time')))
             threads[ns] = NSThread(args=(nsq, set()))
+            threads[ns].nsid = nsid
         targets = threads.values()  # XXX hack
     else:
         nscycle = itertools.cycle(notestreams)
         for idx, ns in zip(xrange(number), nscycle):
-            cli = routeset.Route(ns)
-            if cli:
+            clis = routeset.Route(ns)
+            for cli in clis:
                 nsq = ns.findall('note')
                 nsq.sort(key=lambda x: float(x.get('time')))
                 if ns in threads:

+ 33 - 4
client.py

@@ -31,6 +31,10 @@ parser.add_option('--pg-fullscreen', dest='fullscreen', action='store_true', hel
 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)')
 
 options, args = parser.parse_args()
 
@@ -72,6 +76,7 @@ def GUI(f):
 def pygame_notes():
     import pygame
     import pygame.gfxdraw
+    import colorsys
     pygame.init()
 
     dispinfo = pygame.display.Info()
@@ -103,14 +108,37 @@ 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:
-        disp.fill((0, 0, 0), (BGR_WIDTH, 0, SAMP_WIDTH, HEIGHT))
-        disp.scroll(-1, 0)
-
+        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:
+                    pitchval = float(FREQ - 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 * ((AMP / float(MAX)) ** 2), 1.0)
+                    bgcol = [int(j*255) for j in bgcol]
+                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]
@@ -122,7 +150,7 @@ def pygame_notes():
             else:
                 pitch = 0
             col = [int((AMP / MAX) * 255)] * 3
-            disp.fill(col, (BGR_WIDTH - 1, HEIGHT - pitch * PFAC - PFAC, 1, PFAC))
+            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))
@@ -143,6 +171,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()
 

+ 7 - 3
drums.py

@@ -18,6 +18,7 @@ parser.add_option('-u', '--uid', dest='uid', default='', help='User identifier o
 parser.add_option('-p', '--port', dest='port', default=13676, type='int', help='UDP port to listen on')
 parser.add_option('--repeat', dest='repeat', action='store_true', help='If a note plays longer than a sample length, keep playing the sample')
 parser.add_option('--cut', dest='cut', action='store_true', help='If a note ends within a sample, stop playing that sample immediately')
+parser.add_option('-n', '--max-voices', dest='max_voices', default=-1, type='int', help='Only support this many notes playing simultaneously (earlier ones get dropped)')
 
 options, args = parser.parse_args()
 
@@ -67,7 +68,7 @@ for fname in args:
 if options.verbose:
     print len(DRUMS), 'sounds loaded'
 
-PLAYING = set()
+PLAYING = []
 
 class SampleReader(object):
     def __init__(self, buf, total, amp):
@@ -108,7 +109,7 @@ def gen_data(data, frames, tm, status):
         for i in range(frames):
             fdata[i] += samps[i]
     for src in torem:
-        PLAYING.discard(src)
+        PLAYING.remove(src)
     for i in range(frames):
         fdata[i] = max(MIN, min(MAX, fdata[i]))
     fdata = array.array('i', fdata)
@@ -162,7 +163,10 @@ while True:
         if not options.cut:
             dframes = rframes * ((dframes + rframes - 1) / rframes)
         amp = max(min(options.volume * pkt.as_float(3), 1.0), 0.0)
-        PLAYING.add(SampleReader(rdata, dframes * 4, amp))
+        PLAYING.append(SampleReader(rdata, dframes * 4, amp))
+        if options.max_voices >= 0:
+            while len(PLAYING) > options.max_voices:
+                PLAYING.pop(0)
         #signal.setitimer(signal.ITIMER_REAL, dur)
     elif pkt.cmd == CMD.CAPS:
         data = [0] * 8

+ 8 - 13
mkiv.py

@@ -4,10 +4,6 @@ mkiv -- Make Intervals
 
 This simple script (using python-midi) reads a MIDI file and makes an interval
 (.iv) file (actually XML) that contains non-overlapping notes.
-
-TODO:
--MIDI Control events
--Percussion
 '''
 
 import xml.etree.ElementTree as ET
@@ -48,7 +44,8 @@ parser.add_option('--tempo', dest='tempo', help='Adjust interpretation of tempo
 parser.add_option('--epsilon', dest='epsilon', type='float', help='Don\'t consider overlaps smaller than this number of seconds (which regularly happen due to precision loss)')
 parser.add_option('--vol-pow', dest='vol_pow', type='float', help='Exponent to raise volume changes (adjusts energy per delta volume)')
 parser.add_option('-0', '--keep-empty', dest='keepempty', action='store_true', help='Keep (do not cull) events with 0 duration in the output file')
-parser.set_defaults(tracks=[], perc='GM', deviation=2, tempo='global', modres=0.005, modfdev=2.0, modffreq=8.0, modadev=0.5, modafreq=8.0, stringres=0, stringmax=1024, stringrateon=0.7, stringrateoff=0.01, stringthres=0.02, epsilon=1e-12, vol_pow=2)
+parser.add_option('--no-text', dest='no_text', action='store_true', help='Disable text streams (useful for unusual text encodings)')
+parser.set_defaults(tracks=[], perc='GM', deviation=2, tempo='global', modres=0.005, modfdev=2.0, modffreq=8.0, modadev=0.5, modafreq=8.0, stringres=0, stringmax=1024, stringrateon=0.7, stringrateoff=0.4, stringthres=0.02, epsilon=1e-12, vol_pow=2)
 options, args = parser.parse_args()
 if options.tempo == 'f1':
     options.tempo == 'global'
@@ -690,14 +687,12 @@ for fname in args:
                             ivnote.set('time', str(note.abstime))
                             ivnote.set('dur', str(note.duration))
 
-    ivtext = ET.SubElement(ivstreams, 'stream', type='text')
-    for tev in textstream:
-        text = tev.ev.text
-        # XXX Codec woes and general ET silliness
-        text = text.replace('\0', '')
-        #text = text.decode('latin_1')
-        #text = text.encode('ascii', 'replace')
-        ivev = ET.SubElement(ivtext, 'text', time=str(tev.abstime), type=type(tev.ev).__name__, text=text)
+    if not options.no_text:
+        ivtext = ET.SubElement(ivstreams, 'stream', type='text')
+        for tev in textstream:
+            text = tev.ev.text
+            text = text.decode('utf8')
+            ivev = ET.SubElement(ivtext, 'text', time=str(tev.abstime), type=type(tev.ev).__name__, text=text)
 
     ivaux = ET.SubElement(ivstreams, 'stream')
     ivaux.set('type', 'aux')