Procházet zdrojové kódy

Bugfixes, small features, and better functional voices!

Grissess před 10 roky
rodič
revize
005d2fdf90
3 změnil soubory, kde provedl 118 přidání a 11 odebrání
  1. 12 8
      broadcast.py
  2. 89 0
      client.py
  3. 17 3
      mkiv.py

+ 12 - 8
broadcast.py

@@ -11,8 +11,9 @@ from packet import Packet, CMD, itos
 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('-q', '--quit', dest='quit', action='store_true', help='Instruct all clients to quit')
-parser.add_option('-f', '--factor', dest='factor', type='int', help='Rescale time by this factor (0<f<1 are faster; 0.5 is twice the speed, 2 is half)')
+parser.add_option('-f', '--factor', dest='factor', type='float', default=1.0, help='Rescale time by this factor (0<f<1 are faster; 0.5 is twice the speed, 2 is half)')
 parser.add_option('-r', '--route', dest='routes', action='append', help='Add a routing directive (see --route-help)')
+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('--help-routes', dest='help_routes', action='store_true', help='Show help about routing directives')
 parser.set_defaults(routes=[])
 options, args = parser.parse_args()
@@ -34,10 +35,7 @@ The specifier consists of a comma-separated list of attribute-colon-value pairs,
     exit()
 
 PORT = 13676
-if len(sys.argv) > 2:
-	factor = float(sys.argv[2])
-else:
-	factor = 1
+factor = options.factor
 
 print 'Factor:', factor
 
@@ -86,7 +84,7 @@ if options.test or options.quit:
     exit()
 
 try:
-	iv = ET.parse(sys.argv[1]).getroot()
+	iv = ET.parse(args[0]).getroot()
 except IOError:
 	print 'Bad file'
 	exit()
@@ -98,6 +96,10 @@ print len(clients), 'clients'
 print len(groups), 'groups'
 
 class NSThread(threading.Thread):
+        def wait_for(self, t):
+            if t <= 0:
+                return
+            time.sleep(t)
 	def run(self):
 		nsq, cl = self._Thread__args
 		for note in nsq:
@@ -106,9 +108,11 @@ class NSThread(threading.Thread):
 			vel = int(note.get('vel'))
 			dur = factor*float(note.get('dur'))
 			while time.time() - BASETIME < factor*ttime:
-				time.sleep(factor*ttime - (time.time() - BASETIME))
+				self.wait_for(factor*ttime - (time.time() - BASETIME))
 			s.sendto(str(Packet(CMD.PLAY, int(dur), int((dur*1000000)%1000000), int(440.0 * 2**((pitch-69)/12.0)), vel*2)), cl)
-			time.sleep(dur)
+                        print (time.time() - BASETIME), cl, ': PLAY', pitch, dur, vel
+			self.wait_for(dur - ((time.time() - BASETIME) - factor*ttime))
+                print '% 6.5f'%(time.time() - BASETIME,), cl, ': DONE'
 
 threads = []
 for ns in notestreams:

+ 89 - 0
client.py

@@ -9,12 +9,14 @@ import math
 import struct
 import socket
 import optparse
+import array
 
 from packet import Packet, CMD, stoi
 
 parser = optparse.OptionParser()
 parser.add_option('-t', '--test', dest='test', action='store_true', help='Play a test sequence (440,<rest>,880,440), then exit')
 parser.add_option('-g', '--generator', dest='generator', default='math.sin', help='Set the generator (to a Python expression)')
+parser.add_option('--generators', dest='generators', action='store_true', help='Show the list of generators, then exit')
 parser.add_option('-u', '--uid', dest='uid', default='', help='Set the UID (identifier) of this client in the network')
 parser.add_option('-p', '--port', dest='port', type='int', default=13676, help='Set the port to listen on')
 parser.add_option('-r', '--rate', dest='rate', type='int', default=44100, help='Set the sample rate of the audio device')
@@ -42,6 +44,18 @@ def lin_interp(frm, to, p):
 
 # Generator functions--should be cyclic within [0, 2*math.pi) and return [-1, 1]
 
+GENERATORS = [{'name': 'math.sin', 'args': None, 'desc': 'Sine function'},
+        {'name':'math.cos', 'args': None, 'desc': 'Cosine function'}]
+
+def generator(desc=None, args=None):
+    def inner(f, desc=desc, args=args):
+        if desc is None:
+            desc = f.__doc__
+        GENERATORS.append({'name': f.__name__, 'desc': desc, 'args': args})
+        return f
+    return inner
+
+@generator('Simple triangle wave (peaks/troughs at pi/2, 3pi/2)')
 def tri_wave(theta):
     if theta < math.pi/2:
         return lin_interp(0, 1, theta/(math.pi/2))
@@ -50,12 +64,87 @@ def tri_wave(theta):
     else:
         return lin_interp(-1, 0, (theta-3*math.pi/2)/(math.pi/2))
 
+@generator('Simple square wave (piecewise 1 at x<pi, 0 else)')
 def square_wave(theta):
     if theta < math.pi:
         return 1
     else:
         return -1
 
+@generator('File generator', '(<file>[, <bits=8>[, <signed=True>[, <0=linear interp (default), 1=nearest>[, <swapbytes=False>]]]])')
+class file_samp(object):
+    LINEAR = 0
+    NEAREST = 1
+    TYPES = {8: 'B', 16: 'H', 32: 'L'}
+    def __init__(self, fname, bits=8, signed=True, samp=LINEAR, swab=False):
+        tp = self.TYPES[bits]
+        if signed:
+            tp = tp.lower()
+        self.max = float((2 << bits) - 1)
+        self.buffer = array.array(tp)
+        self.buffer.fromstring(open(fname, 'rb').read())
+        if swab:
+            self.buffer.byteswap()
+        self.samp = samp
+    def __call__(self, theta):
+        norm = theta / (2*math.pi)
+        if self.samp == self.LINEAR:
+            v = norm*len(self.buffer)
+            l = int(math.floor(v))
+            h = int(math.ceil(v))
+            if l == h:
+                return self.buffer[l]/self.max
+            if h >= len(self.buffer):
+                h = 0
+            return lin_interp(self.buffer[l], self.buffer[h], v-l)/self.max
+        elif self.samp == self.NEAREST:
+            return self.buffer[int(math.ceil(norm*len(self.buffer) - 0.5))]/self.max
+
+@generator('Harmonics generator (adds overtones at f, 2f, 3f, 4f, etc.)', '(<generator>, <amplitude of f>, <amp 2f>, <amp 3f>, ...)')
+class harmonic(object):
+    def __init__(self, gen, *spectrum):
+        self.gen = gen
+        self.spectrum = spectrum
+    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)])))
+
+@generator('Mix generator', '(<generator>[, <amp>], [<generator>[, <amp>], [...]])')
+class mixer(object):
+    def __init__(self, *specs):
+        self.pairs = []
+        i = 0
+        while i < len(specs):
+            if i+1 < len(specs) and isinstance(specs[i+1], (float, int)):
+                pair = (specs[i], specs[i+1])
+                i += 2
+            else:
+                pair = (specs[i], None)
+                i += 1
+            self.pairs.append(pair)
+        tamp = 1 - min(1, sum([amp for gen, amp in self.pairs if amp is not None]))
+        parts = float(len([None for gen, amp in self.pairs if amp is None]))
+        for idx, pair in enumerate(self.pairs):
+            if pair[1] is None:
+                self.pairs[idx] = (pair[0], tamp / parts)
+    def __call__(self, theta):
+        return max(-1, min(1, sum([amp*gen(theta) for gen, amp in self.pairs])))
+
+@generator('Phase offset generator (in radians; use math.pi)', '(<generator>, <offset>)')
+class phase_off(object):
+    def __init__(self, gen, offset):
+        self.gen = gen
+        self.offset = offset
+    def __call__(self, theta):
+        return self.gen((theta + self.offset) % (2*math.pi))
+
+if options.generators:
+    for item in GENERATORS:
+        print item['name'],
+        if item['args'] is not None:
+            print item['args'],
+        print '--', item['desc']
+    exit()
+
 #generator = math.sin
 #generator = tri_wave
 #generator = square_wave

+ 17 - 3
mkiv.py

@@ -27,6 +27,7 @@ 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', 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('-f', '--fuckit', dest='fuckit', action='store_true', help='Use the Python Error Steamroller when importing MIDIs (useful for extended formats)')
 parser.set_defaults(tracks=[])
 options, args = parser.parse_args()
 
@@ -65,8 +66,15 @@ if not args:
     parser.print_usage()
     exit()
 
+if options.fuckit:
+    import fuckit
+    midi.read_midifile = fuckit(midi.read_midifile)
+
 for fname in args:
     pat = midi.read_midifile(fname)
+    if pat is None:
+        print fname, ': Too fucked to continue'
+        continue
     iv = ET.Element('iv')
     iv.set('version', '1')
     iv.set('src', os.path.basename(fname))
@@ -78,12 +86,17 @@ for fname in args:
         pat = midi.Pattern(resolution=old_pat.resolution)
         for track in old_pat:
             chan_map = {}
+            last_abstick = {}
+            absticks = 0
             for ev in track:
+                absticks += ev.tick
                 if isinstance(ev, midi.Event):
+                    tick = absticks - last_abstick.get(ev.channel, 0)
+                    last_abstick[ev.channel] = absticks
                     if options.chanskeep:
-                        newev = ev.copy()
+                        newev = ev.copy(tick = tick)
                     else:
-                        newev = ev.copy(channel=1)
+                        newev = ev.copy(channel=1, tick = tick)
                     chan_map.setdefault(ev.channel, midi.Track()).append(newev)
                 else: # MetaEvent
                     for trk in chan_map.itervalues():
@@ -122,7 +135,7 @@ for fname in args:
             else:
                 if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0:
                     ev.__class__ = midi.NoteOffEvent #XXX Oww
-                bpm = filter(lambda pair: pair[0] <= absticks, bpm_at.items())[-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)
                 absticks += ev.tick
                 events.append(MergeEvent(ev, tidx, abstime))
@@ -231,6 +244,7 @@ for fname in args:
         print ('<anonymous>' if group.name is None else group.name), '<=', group.filter, '(', len(group.streams), 'streams)'
 
     print 'Generated %d streams in %d groups'%(sum(map(lambda x: len(x.streams), notegroups)), len(notegroups))
+    print 'Playtime:', lastabstime, 'seconds'
 
 ##### Write to XML and exit #####