Browse Source

Bugfixes, graphics, and new generators

Grissess 9 years ago
parent
commit
b6ab9fcbec
4 changed files with 197 additions and 25 deletions
  1. 94 5
      broadcast.py
  2. 9 1
      client.py
  3. 58 19
      mkiv.py
  4. 36 0
      shiv.py

+ 94 - 5
broadcast.py

@@ -4,6 +4,7 @@ import struct
 import time
 import time
 import xml.etree.ElementTree as ET
 import xml.etree.ElementTree as ET
 import threading
 import threading
+import thread
 import optparse
 import optparse
 import random
 import random
 
 
@@ -11,6 +12,7 @@ from packet import Packet, CMD, itos
 
 
 parser = optparse.OptionParser()
 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('-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('-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('--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('-R', '--random', dest='random', type='float', help='Generate random notes at approximately this period')
 parser.add_option('-R', '--random', dest='random', type='float', help='Generate random notes at approximately this period')
@@ -31,8 +33,12 @@ parser.add_option('-r', '--route', dest='routes', action='append', help='Add a r
 parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Be verbose; dump events and actual time (can slow down performance!)')
 parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Be verbose; dump events and actual time (can slow down performance!)')
 parser.add_option('-W', '--wait-time', dest='wait_time', type='float', help='How long to wait for clients to initially respond (delays all broadcasts)')
 parser.add_option('-W', '--wait-time', dest='wait_time', type='float', help='How long to wait for clients to initially respond (delays all broadcasts)')
 parser.add_option('-B', '--bind-addr', dest='bind_addr', help='The IP address (or IP:port) to bind to (influences the network to send to)')
 parser.add_option('-B', '--bind-addr', dest='bind_addr', help='The IP address (or IP:port) to bind to (influences the network to send to)')
+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-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.add_option('--help-routes', dest='help_routes', action='store_true', help='Show help about routing directives')
-parser.set_defaults(routes=[], random=0.0, rand_low=80, rand_high=2000, live=None, factor=1.0, duration=1.0, volume=255, wait_time=0.25, play=[], transpose=0, seek=0.0, bind_addr='')
+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=255, wait_time=0.25, play=[], transpose=0, seek=0.0, bind_addr='', pg_width = 0, pg_height = 0)
 options, args = parser.parse_args()
 options, args = parser.parse_args()
 
 
 if options.help_routes:
 if options.help_routes:
@@ -51,6 +57,66 @@ The syntax for that specification resembles the following:
 The specifier consists of a comma-separated list of attribute-colon-value pairs, followed by an equal sign. After this is a comma-separated list of exclusivities paired with the name of a stream group as specified in the file. The above example shows that stream groups "bass", "treble", and "beeps" will be routed to clients with UID "bass", "treble", and TYPE "BEEP" respectively. Additionally, TYPE "BEEP" will receive tracks 4 and 6 (indices 3 and 5) of the MIDI file (presumably split with -T), and that these three groups are exclusively to be routed to TYPE "BEEP" clients only (the broadcaster will drop the stream if no more are available), as opposed to the preference of the bass and treble groups, which may be routed onto other stream clients if they are available. Finally, the last route says that all "noise" UID clients should not proceed any further (receiving "null" streams) instead. Order is important; if a "noise" client already received a stream (such as "+beeps"), then it would receive that route with priority.'''
 The specifier consists of a comma-separated list of attribute-colon-value pairs, followed by an equal sign. After this is a comma-separated list of exclusivities paired with the name of a stream group as specified in the file. The above example shows that stream groups "bass", "treble", and "beeps" will be routed to clients with UID "bass", "treble", and TYPE "BEEP" respectively. Additionally, TYPE "BEEP" will receive tracks 4 and 6 (indices 3 and 5) of the MIDI file (presumably split with -T), and that these three groups are exclusively to be routed to TYPE "BEEP" clients only (the broadcaster will drop the stream if no more are available), as opposed to the preference of the bass and treble groups, which may be routed onto other stream clients if they are available. Finally, the last route says that all "noise" UID clients should not proceed any further (receiving "null" streams) instead. Order is important; if a "noise" client already received a stream (such as "+beeps"), then it would receive that route with priority.'''
     exit()
     exit()
 
 
+GUIS = {}
+
+def gui_pygame():
+    print 'Starting pygame GUI...'
+    import pygame, colorsys
+    pygame.init()
+    print 'Pygame init'
+
+    dispinfo = pygame.display.Info()
+    DISP_WIDTH = 640
+    DISP_HEIGHT = 480
+    if dispinfo.current_h > 0 and dispinfo.current_w > 0:
+        DISP_WIDTH = dispinfo.current_w
+        DISP_HEIGHT = dispinfo.current_h
+    print 'Pygame info'
+
+    WIDTH = DISP_WIDTH
+    if options.pg_width > 0:
+        WIDTH = options.pg_width
+    HEIGHT = DISP_HEIGHT
+    if options.pg_height > 0:
+        HEIGHT = options.pg_height
+
+    flags = 0
+    if options.fullscreen:
+        flags |= pygame.FULLSCREEN
+
+    disp = pygame.display.set_mode((WIDTH, HEIGHT), flags)
+    print 'Disp acquire'
+
+    PFAC = HEIGHT / 128.0
+
+    clock = pygame.time.Clock()
+
+    print 'Pygame GUI initialized, running...'
+
+    while True:
+
+        disp.scroll(-1, 0)
+        disp.fill((0, 0, 0), (WIDTH - 1, 0, 1, HEIGHT))
+        idx = 0
+        for cli, note in sorted(playing_notes.items(), key = lambda pair: pair[0]):
+            pitch = note[0]
+            col = colorsys.hls_to_rgb(float(idx) / len(clients), note[1]/512.0, 1.0)
+            col = [int(i*255) for i in col]
+            disp.fill(col, (WIDTH - 1, HEIGHT - pitch * PFAC - PFAC, 1, PFAC))
+            idx += 1
+        pygame.display.flip()
+
+        for ev in pygame.event.get():
+            if ev.type == pygame.KEYDOWN:
+                if ev.key == pygame.K_ESCAPE:
+                    thread.interrupt_main()
+                    pygame.quit()
+                    exit()
+
+        clock.tick(60)
+
+GUIS['pygame'] = gui_pygame
+
 PORT = 13676
 PORT = 13676
 factor = options.factor
 factor = options.factor
 
 
@@ -78,6 +144,10 @@ try:
 except socket.timeout:
 except socket.timeout:
 	pass
 	pass
 
 
+playing_notes = {}
+for cli in clients:
+    playing_notes[cli] = (0, 0)
+
 print len(clients), 'detected clients'
 print len(clients), 'detected clients'
 
 
 print 'Clients:'
 print 'Clients:'
@@ -96,15 +166,21 @@ for cl in clients:
         uid_groups.setdefault(uid, []).append(cl)
         uid_groups.setdefault(uid, []).append(cl)
         type_groups.setdefault(tp, []).append(cl)
         type_groups.setdefault(tp, []).append(cl)
 	if options.test:
 	if options.test:
-		s.sendto(str(Packet(CMD.PLAY, 0, 250000, 440, options.volume)), cl)
+                ts, tms = int(options.test_delay), int(options.test_delay * 1000000) % 1000000
+		s.sendto(str(Packet(CMD.PLAY, ts, tms, 440, options.volume)), cl)
                 if not options.sync_test:
                 if not options.sync_test:
-                    time.sleep(0.25)
-                    s.sendto(str(Packet(CMD.PLAY, 0, 250000, 880, options.volume)), cl)
+                    time.sleep(options.test_delay)
+                    s.sendto(str(Packet(CMD.PLAY, ts, tms, 880, options.volume)), cl)
 	if options.quit:
 	if options.quit:
 		s.sendto(str(Packet(CMD.QUIT)), cl)
 		s.sendto(str(Packet(CMD.QUIT)), cl)
         if options.silence:
         if options.silence:
                 s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0)), cl)
                 s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0)), cl)
 
 
+if options.gui:
+    gui_thr = threading.Thread(target=GUIS[options.gui], args=())
+    gui_thr.setDaemon(True)
+    gui_thr.start()
+
 if options.play:
 if options.play:
     for i, val in enumerate(options.play):
     for i, val in enumerate(options.play):
         if val.startswith('@'):
         if val.startswith('@'):
@@ -134,6 +210,9 @@ if options.random > 0:
         time.sleep(options.random)
         time.sleep(options.random)
 
 
 if options.live or options.list_live:
 if options.live or options.list_live:
+    if options.gui:
+        print 'Waiting a second for GUI init...'
+        time.sleep(3.0)
     import midi
     import midi
     from midi import sequencer
     from midi import sequencer
     S = sequencer.S
     S = sequencer.S
@@ -149,9 +228,12 @@ if options.live or options.list_live:
     if client or port:
     if client or port:
         seq.subscribe_port(client, port)
         seq.subscribe_port(client, port)
     seq.start_sequencer()
     seq.start_sequencer()
-    seq.set_nonblock(False)
+    if not options.gui:  # FIXME
+        seq.set_nonblock(False)
     while True:
     while True:
         ev = S.event_input(seq.client)
         ev = S.event_input(seq.client)
+        if ev is None:
+            time.sleep(0)
         event = None
         event = None
         if ev:
         if ev:
             if options.verbose:
             if options.verbose:
@@ -187,6 +269,7 @@ if options.live or options.list_live:
                 cli = sorted(inactive_set)[0]
                 cli = sorted(inactive_set)[0]
                 s.sendto(str(Packet(CMD.PLAY, 65535, 0, int(440.0 * 2**((event.pitch-69)/12.0)), 2*event.velocity)), cli)
                 s.sendto(str(Packet(CMD.PLAY, 65535, 0, int(440.0 * 2**((event.pitch-69)/12.0)), 2*event.velocity)), cli)
                 active_set.setdefault(event.pitch, []).append(cli)
                 active_set.setdefault(event.pitch, []).append(cli)
+                playing_notes[cli] = (event.pitch, 2*event.velocity)
                 if options.verbose:
                 if options.verbose:
                     print 'LIVE:', event.pitch, '+ =>', active_set[event.pitch]
                     print 'LIVE:', event.pitch, '+ =>', active_set[event.pitch]
             elif isinstance(event, midi.NoteOffEvent):
             elif isinstance(event, midi.NoteOffEvent):
@@ -198,6 +281,7 @@ if options.live or options.list_live:
                     continue
                     continue
                 cli = active_set[event.pitch].pop()
                 cli = active_set[event.pitch].pop()
                 s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0)), cli)
                 s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0)), cli)
+                playing_notes[cli] = (0, 0)
                 if options.verbose:
                 if options.verbose:
                     print 'LIVE:', event.pitch, '- =>', active_set[event.pitch]
                     print 'LIVE:', event.pitch, '- =>', active_set[event.pitch]
                     if sustain_status:
                     if sustain_status:
@@ -214,6 +298,7 @@ if options.live or options.list_live:
                                 continue
                                 continue
                             for cli in active_set[pitch]:
                             for cli in active_set[pitch]:
                                 s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0)), cli)
                                 s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0)), cli)
+                                playing_notes[cli] = (0, 0)
                             del active_set[pitch]
                             del active_set[pitch]
                         deferred_set.clear()
                         deferred_set.clear()
 
 
@@ -238,6 +323,8 @@ for fname in args:
                 self.map = uid_groups
                 self.map = uid_groups
             elif fattr == 'T':
             elif fattr == 'T':
                 self.map = type_groups
                 self.map = type_groups
+            elif fattr == '0':
+                self.map = {}
             else:
             else:
                 raise ValueError('Not a valid attribute specifier: %r'%(fattr,))
                 raise ValueError('Not a valid attribute specifier: %r'%(fattr,))
             self.value = fvalue
             self.value = fvalue
@@ -385,7 +472,9 @@ for fname in args:
                             s.sendto(str(Packet(CMD.PLAY, int(dur), int((dur*1000000)%1000000), int(440.0 * 2**((pitch-69)/12.0)), int(vel*2 * options.volume/255.0))), cl)
                             s.sendto(str(Packet(CMD.PLAY, int(dur), int((dur*1000000)%1000000), int(440.0 * 2**((pitch-69)/12.0)), int(vel*2 * options.volume/255.0))), cl)
                             if options.verbose:
                             if options.verbose:
                                 print (time.time() - BASETIME), cl, ': PLAY', pitch, dur, vel
                                 print (time.time() - BASETIME), cl, ': PLAY', pitch, dur, vel
+                            playing_notes[cl] = (pitch, vel*2)
                             self.wait_for(dur - ((time.time() - BASETIME) - factor*ttime))
                             self.wait_for(dur - ((time.time() - BASETIME) - factor*ttime))
+                            playing_notes[cl] = (0, 0)
                     if options.verbose:
                     if options.verbose:
                         print '% 6.5f'%(time.time() - BASETIME,), cl, ': DONE'
                         print '% 6.5f'%(time.time() - BASETIME,), cl, ': DONE'
 
 

+ 9 - 1
client.py

@@ -78,7 +78,7 @@ def pygame_notes():
         SAMP_WIDTH = options.samp_width
         SAMP_WIDTH = options.samp_width
     BGR_WIDTH = DISP_WIDTH / 2
     BGR_WIDTH = DISP_WIDTH / 2
     if options.bgr_width > 0:
     if options.bgr_width > 0:
-        NGR_WIDTH = options.bgr_width
+        BGR_WIDTH = options.bgr_width
     HEIGHT = DISP_HEIGHT
     HEIGHT = DISP_HEIGHT
     if options.height > 0:
     if options.height > 0:
         HEIGHT = options.height
         HEIGHT = options.height
@@ -214,6 +214,14 @@ class harmonic(object):
     def __call__(self, theta):
     def __call__(self, theta):
         return max(-1, min(1, sum([amp*self.gen((i+1)*theta % (2*math.pi)) for i, amp in enumerate(self.spectrum)])))
         return max(-1, min(1, sum([amp*self.gen((i+1)*theta % (2*math.pi)) for i, amp in enumerate(self.spectrum)])))
 
 
+@generator('General harmonics generator (adds arbitrary overtones)', '(<generator>, <factor of f>, <amplitude>, <factor>, <amplitude>, ...)')
+class genharmonic(object):
+    def __init__(self, gen, *harmonics):
+        self.gen = gen
+        self.harmonics = zip(harmonics[::2], harmonics[1::2])
+    def __call__(self, theta):
+        return max(-1, min(1, sum([amp * self.gen(i * theta % (2*math.pi)) for i, amp in self.harmonics])))
+
 @generator('Mix generator', '(<generator>[, <amp>], [<generator>[, <amp>], [...]])')
 @generator('Mix generator', '(<generator>[, <amp>], [<generator>[, <amp>], [...]])')
 class mixer(object):
 class mixer(object):
     def __init__(self, *specs):
     def __init__(self, *specs):

+ 58 - 19
mkiv.py

@@ -6,10 +6,8 @@ This simple script (using python-midi) reads a MIDI file and makes an interval
 (.iv) file (actually XML) that contains non-overlapping notes.
 (.iv) file (actually XML) that contains non-overlapping notes.
 
 
 TODO:
 TODO:
--Reserve channels by track
--Reserve channels by MIDI channel
--Pitch limits for channels
 -MIDI Control events
 -MIDI Control events
+-Percussion
 '''
 '''
 
 
 import xml.etree.ElementTree as ET
 import xml.etree.ElementTree as ET
@@ -27,10 +25,12 @@ parser.add_option('-c', '--preserve-channels', dest='chanskeep', action='store_t
 parser.add_option('-T', '--track-split', dest='tracks', action='append_const', const=TRACKS, help='Ensure all tracks are on non-mutual streams')
 parser.add_option('-T', '--track-split', dest='tracks', action='append_const', const=TRACKS, help='Ensure all tracks are on non-mutual streams')
 parser.add_option('-t', '--track', dest='tracks', action='append', help='Reserve an exclusive set of streams for certain conditions (try --help-conds)')
 parser.add_option('-t', '--track', dest='tracks', action='append', help='Reserve an exclusive set of streams for certain conditions (try --help-conds)')
 parser.add_option('--help-conds', dest='help_conds', action='store_true', help='Print help on filter conditions for streams')
 parser.add_option('--help-conds', dest='help_conds', action='store_true', help='Print help on filter conditions for streams')
-parser.add_option('-P', '--no-percussion', dest='no_perc', action='store_true', help='Don\'t try to filter percussion events out')
+parser.add_option('-P', '--percussion', dest='perc', help='Which percussion standard to use to automatically filter to "perc" (GM, GM2, or none)')
 parser.add_option('-f', '--fuckit', dest='fuckit', action='store_true', help='Use the Python Error Steamroller when importing MIDIs (useful for extended formats)')
 parser.add_option('-f', '--fuckit', dest='fuckit', action='store_true', help='Use the Python Error Steamroller when importing MIDIs (useful for extended formats)')
 parser.add_option('-n', '--target-num', dest='repeaterNumber', type='int', help='Target count of devices')
 parser.add_option('-n', '--target-num', dest='repeaterNumber', type='int', help='Target count of devices')
-parser.set_defaults(tracks=[], repeaterNumber=1)
+parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Be verbose; show important parts about the MIDI scheduling process')
+parser.add_option('-d', '--debug', dest='debug', action='store_true', help='Debugging output; show excessive output about the MIDI scheduling process')
+parser.set_defaults(tracks=[], repeaterNumber=1, perc='GM')
 options, args = parser.parse_args()
 options, args = parser.parse_args()
 
 
 if options.help_conds:
 if options.help_conds:
@@ -40,9 +40,14 @@ Every filter is an expression; internally, this expression is evaluated as the b
 The "ev" object will be a MergeEvent with the following properties:
 The "ev" object will be a MergeEvent with the following properties:
 -ev.tidx: the originating track index (starting at 0)
 -ev.tidx: the originating track index (starting at 0)
 -ev.abstime: the real time in seconds of this event relative to the beginning of playback
 -ev.abstime: the real time in seconds of this event relative to the beginning of playback
+-ev.bank: the selected bank (all bits)
+-ev.prog: the selected program
 -ev.ev: a midi.NoteOnEvent:
 -ev.ev: a midi.NoteOnEvent:
     -ev.ev.pitch: the MIDI pitch
     -ev.ev.pitch: the MIDI pitch
     -ev.ev.velocity: the MIDI velocity
     -ev.ev.velocity: the MIDI velocity
+    -ev.ev.channel: the MIDI channel
+
+All valid Python expressions are accepted. Take care to observe proper shell escaping.
 
 
 Specifying a -t <group>=<filter> will group all streams under a filter; if the <group> part is omitted, no group will be added.
 Specifying a -t <group>=<filter> will group all streams under a filter; if the <group> part is omitted, no group will be added.
 For example:
 For example:
@@ -122,31 +127,59 @@ for fname in args:
     print 'Merging events...'
     print 'Merging events...'
 
 
     class MergeEvent(object):
     class MergeEvent(object):
-        __slots__ = ['ev', 'tidx', 'abstime']
-        def __init__(self, ev, tidx, abstime):
+        __slots__ = ['ev', 'tidx', 'abstime', 'bank', 'prog']
+        def __init__(self, ev, tidx, abstime, bank, prog):
             self.ev = ev
             self.ev = ev
             self.tidx = tidx
             self.tidx = tidx
             self.abstime = abstime
             self.abstime = abstime
+            self.bank = bank
+            self.prog = prog
         def __repr__(self):
         def __repr__(self):
             return '<ME %r in %d @%f>'%(self.ev, self.tidx, self.abstime)
             return '<ME %r in %d @%f>'%(self.ev, self.tidx, self.abstime)
 
 
     events = []
     events = []
     bpm_at = {0: 120}
     bpm_at = {0: 120}
+    cur_bank = [[0 for i in range(16)] for j in range(len(pat))]
+    cur_prog = [[0 for i in range(16)] for j in range(len(pat))]
+    chg_bank = [[0 for i in range(16)] for j in range(len(pat))]
+    chg_prog = [[0 for i in range(16)] for j in range(len(pat))]
+    ev_cnts = [[0 for i in range(16)] for j in range(len(pat))]
+    tnames = [''] * len(pat)
 
 
     for tidx, track in enumerate(pat):
     for tidx, track in enumerate(pat):
         abstime = 0
         abstime = 0
         absticks = 0
         absticks = 0
         for ev in track:
         for ev in track:
+            if options.debug:
+                print ev
             if isinstance(ev, midi.SetTempoEvent):
             if isinstance(ev, midi.SetTempoEvent):
                 absticks += ev.tick
                 absticks += ev.tick
                 bpm_at[absticks] = ev.bpm
                 bpm_at[absticks] = ev.bpm
-            else:
+            elif isinstance(ev, midi.ProgramChangeEvent):
+                cur_prog[tidx][ev.channel] = ev.value
+                chg_prog[tidx][ev.channel] += 1
+            elif isinstance(ev, midi.ControlChangeEvent):
+                if ev.control == 0:
+                    cur_bank[tidx][ev.channel] = (0x3F80 & cur_bank[tidx][ev.channel]) | ev.value
+                    chg_bank[tidx][ev.channel] += 1
+                elif ev.control == 32:
+                    cur_bank[tidx][ev.channel] = (0x3F & cur_bank[tidx][ev.channel]) | (ev.value << 7)
+                    chg_bank[tidx][ev.channel] += 1
+            elif isinstance(ev, midi.TrackNameEvent):
+                tnames[tidx] = ev.text
+            elif isinstance(ev, midi.Event):
                 if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0:
                 if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0:
                     ev.__class__ = midi.NoteOffEvent #XXX Oww
                     ev.__class__ = midi.NoteOffEvent #XXX Oww
                 bpm = filter(lambda pair: pair[0] <= absticks, sorted(bpm_at.items(), key=lambda pair: pair[0]))[-1][1]
                 bpm = filter(lambda pair: pair[0] <= absticks, sorted(bpm_at.items(), key=lambda pair: pair[0]))[-1][1]
                 abstime += (60.0 * ev.tick) / (bpm * pat.resolution)
                 abstime += (60.0 * ev.tick) / (bpm * pat.resolution)
                 absticks += ev.tick
                 absticks += ev.tick
-                events.append(MergeEvent(ev, tidx, abstime))
+                events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel]))
+                ev_cnts[tidx][ev.channel] += 1
+
+    if options.verbose:
+        print 'Track name, event count, final banks, bank changes, final programs, program changes:'
+        for tidx, tname in enumerate(tnames):
+            print tidx, ':', tname, ',', ','.join(map(str, ev_cnts[tidx])), ',', ','.join(map(str, cur_bank[tidx])), ',', ','.join(map(str, chg_bank[tidx])), ',', ','.join(map(str, cur_prog[tidx])), ',', ','.join(map(str, chg_prog[tidx]))
 
 
     print 'Sorting events...'
     print 'Sorting events...'
 
 
@@ -158,7 +191,7 @@ for fname in args:
     class DurationEvent(MergeEvent):
     class DurationEvent(MergeEvent):
         __slots__ = ['duration']
         __slots__ = ['duration']
         def __init__(self, me, dur):
         def __init__(self, me, dur):
-            MergeEvent.__init__(self, me.ev, me.tidx, me.abstime)
+            MergeEvent.__init__(self, me.ev, me.tidx, me.abstime, me.bank, me.prog)
             self.duration = dur
             self.duration = dur
 
 
     class NoteStream(object):
     class NoteStream(object):
@@ -200,8 +233,13 @@ for fname in args:
     notegroups = []
     notegroups = []
     auxstream = []
     auxstream = []
 
 
-    if not options.no_perc:
-        notegroups.append(NSGroup(filter = lambda mev: mev.ev.channel == 10, name='perc'))
+    if options.perc and options.perc != 'none':
+        if options.perc == 'GM':
+            notegroups.append(NSGroup(filter = lambda mev: mev.ev.channel == 9, name='perc'))
+        elif options.perc == 'GM2':
+            notegroups.append(NSGroup(filter = lambda mev: mev.bank == 15360, name='perc'))
+        else:
+            print 'Unrecognized --percussion option %r; should be GM, GM2, or none'%(options.perc,)
 
 
     for spec in options.tracks:
     for spec in options.tracks:
         if spec is TRACKS:
         if spec is TRACKS:
@@ -214,9 +252,10 @@ for fname in args:
                 name = None
                 name = None
             notegroups.append(NSGroup(filter = eval("lambda ev: "+spec), name = name))
             notegroups.append(NSGroup(filter = eval("lambda ev: "+spec), name = name))
 
 
-    print 'Initial group mappings:'
-    for group in notegroups:
-        print ('<anonymous>' if group.name is None else group.name), '<=', group.filter
+    if options.verbose:
+        print 'Initial group mappings:'
+        for group in notegroups:
+            print ('<anonymous>' if group.name is None else group.name)
 
 
     for mev in events:
     for mev in events:
         if isinstance(mev.ev, midi.NoteOnEvent):
         if isinstance(mev.ev, midi.NoteOnEvent):
@@ -250,9 +289,10 @@ for fname in args:
                 print 'WARNING: Active notes at end of playback.'
                 print 'WARNING: Active notes at end of playback.'
                 ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime))
                 ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime))
 
 
-    print 'Final group mappings:'
-    for group in notegroups:
-        print ('<anonymous>' if group.name is None else group.name), '<=', group.filter, '(', len(group.streams), 'streams)'
+    if options.verbose:
+        print 'Final group mappings:'
+        for group in notegroups:
+            print ('<anonymous>' if group.name is None else group.name), '<=', '(', len(group.streams), 'streams)'
 
 
     print 'Generated %d streams in %d groups'%(sum(map(lambda x: len(x.streams), notegroups)), len(notegroups))
     print 'Generated %d streams in %d groups'%(sum(map(lambda x: len(x.streams), notegroups)), len(notegroups))
     print 'Playtime:', lastabstime, 'seconds'
     print 'Playtime:', lastabstime, 'seconds'
@@ -289,7 +329,6 @@ for fname in args:
         	       		ivnote.set('time', str(note.abstime))
         	       		ivnote.set('time', str(note.abstime))
  	               		ivnote.set('dur', str(note.duration))
  	               		ivnote.set('dur', str(note.duration))
 			x+=1
 			x+=1
-			print x
 			if(x>=options.repeaterNumber and options.repeaterNumber!=1):
 			if(x>=options.repeaterNumber and options.repeaterNumber!=1):
 				break
 				break
 		if(x>=options.repeaterNumber and options.repeaterNumber!=1):
 		if(x>=options.repeaterNumber and options.repeaterNumber!=1):

+ 36 - 0
shiv.py

@@ -42,3 +42,39 @@ for fname in args:
     print 'File :', fname
     print 'File :', fname
     print '\t<computing...>'
     print '\t<computing...>'
 
 
+    if options.meta:
+        print 'Metatrack:',
+        meta = iv.find('./meta')
+        if meta:
+            print 'exists'
+            print '\tBPM track:',
+            bpms = meta.find('./bpms')
+            if bpms:
+                print 'exists'
+                for elem in bpms.iterfind('./bpm'):
+                    print '\t\tAt ticks {}, time {}: {} bpm'.format(elem.get('ticks'), elem.get('time'), elem.get('bpm'))
+
+    if not (options.number or options.groups or options.notes or options.histogram or options.histogram_tracks or options.duration or options.duty_cycle):
+        continue
+
+    streams = iv.findall('./streams/stream')
+    notestreams = [s for s in streams if s.get('type') == 'ns']
+    if options.number:
+        print 'Stream count:'
+        print '\tNotestreams:', len(notestreams)
+        print '\tTotal:', len(streams)
+
+    if not (options.groups or options.notes or options.histogram or options.histogram_tracks or options.duration or options.duty_cycle):
+        continue
+
+    if options.groups:
+        groups = {}
+        for s in notestreams:
+            group = s.get('group', '<anonymous')
+            groups[group] = groups.get(group, 0) + 1
+        print 'Groups:'
+        for name, cnt in groups.iteritems():
+            print '\t{} ({} streams)'.format(name, cnt)
+
+    if not (options.notes or options.histogram or options.histogram_tracks or options.duration or options.duty_cycle):
+        continue