diff options
| -rw-r--r-- | broadcast.py | 91 | ||||
| -rw-r--r-- | client.py | 37 | ||||
| -rw-r--r-- | drums.py | 10 | ||||
| -rw-r--r-- | mkiv.py | 21 | 
4 files changed, 108 insertions, 51 deletions
| diff --git a/broadcast.py b/broadcast.py index c8682da..2c59304 100644 --- a/broadcast.py +++ b/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: @@ -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() @@ -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 @@ -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') | 
