瀏覽代碼

Adding articulation parameters

Graham Northup 7 年之前
父節點
當前提交
fac3853e5f
共有 4 個文件被更改,包括 117 次插入13 次删除
  1. 22 2
      broadcast.py
  2. 24 2
      client.py
  3. 70 9
      mkiv.py
  4. 1 0
      packet.py

+ 22 - 2
broadcast.py

@@ -609,6 +609,16 @@ for fname in args:
                     i += 1
                     i += 1
                     note = nsq.pop(0)
                     note = nsq.pop(0)
                     ttime = float(note.get('time'))
                     ttime = float(note.get('time'))
+                    if note.tag == 'art':
+                        val = float(note.get('value'))
+                        idx = int(note.get('index'))
+                        global_ = note.get('global') is not None
+                        if not options.dry:
+                            for cl in cls:
+                                s.sendto(str(Packet(CMD.ARTP, OBLIGATE_POLYPHONE if global_ else cl[2], idx, val)), cl[:2])
+                        if options.verbose:
+                            print (play_time() - BASETIME), cl, ': ARTP', cl[2], idx, val
+                        continue
                     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'))
@@ -666,6 +676,16 @@ for fname in args:
                     nsq, cls = self._Thread__args
                     nsq, cls = self._Thread__args
                     for note in nsq:
                     for note in nsq:
                             ttime = float(note.get('time'))
                             ttime = float(note.get('time'))
+                            if note.tag == 'art':
+                                val = float(note.get('value'))
+                                idx = int(note.get('index'))
+                                global_ = note.get('global') is not None
+                                if not options.dry:
+                                    for cl in cls:
+                                        s.sendto(str(Packet(CMD.ARTP, OBLIGATE_POLYPHONE if global_ else cl[2], idx, val)), cl[:2])
+                                if options.verbose:
+                                    print (play_time() - BASETIME), cl, ': ARTP', cl[2], idx, val
+                                continue
                             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'))
@@ -697,7 +717,7 @@ for fname in args:
         for idx, ns in zip(xrange(number), nscycle):
         for idx, ns in zip(xrange(number), nscycle):
             clis = routeset.Route(ns)
             clis = routeset.Route(ns)
             for cli in clis:
             for cli in clis:
-                nsq = ns.findall('note')
+                nsq = ns.findall('*')
                 nsq.sort(key=lambda x: float(x.get('time')))
                 nsq.sort(key=lambda x: float(x.get('time')))
                 if ns in threads:
                 if ns in threads:
                     threads[ns]._Thread__args[1].add(cli)
                     threads[ns]._Thread__args[1].add(cli)
@@ -710,7 +730,7 @@ for fname in args:
             print thr._Thread__args[1]
             print thr._Thread__args[1]
 
 
     BASETIME = play_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', 0.0)) + float(n.get('dur', 0.0)) 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:
         for thr in threads.values():
         for thr in threads.values():

+ 24 - 2
client.py

@@ -15,7 +15,7 @@ import threading
 import thread
 import thread
 import colorsys
 import colorsys
 
 
-from packet import Packet, CMD, PLF, stoi
+from packet import Packet, CMD, PLF, stoi, OBLIGATE_POLYPHONE
 
 
 parser = optparse.OptionParser()
 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('-t', '--test', dest='test', action='store_true', help='Play a test sequence (440,<rest>,880,440), then exit')
@@ -33,6 +33,7 @@ parser.add_option('-C', '--chorus', dest='chorus', default=0.0, type='float', he
 parser.add_option('--vibrato', dest='vibrato', default=0.0, type='float', help='Apply periodic perturbances in pitch space by this amplitude (in MIDI pitches)')
 parser.add_option('--vibrato', dest='vibrato', default=0.0, type='float', help='Apply periodic perturbances in pitch space by this amplitude (in MIDI pitches)')
 parser.add_option('--vibrato-freq', dest='vibrato_freq', default=6.0, type='float', help='Frequency of the vibrato perturbances in Hz')
 parser.add_option('--vibrato-freq', dest='vibrato_freq', default=6.0, type='float', help='Frequency of the vibrato perturbances in Hz')
 parser.add_option('--fmul', dest='fmul', default=1.0, type='float', help='Multiply requested frequencies by this amount')
 parser.add_option('--fmul', dest='fmul', default=1.0, type='float', help='Multiply requested frequencies by this amount')
+parser.add_option('--narts', dest='narts', default=64, type='int', help='Store this many articulation parameters for generator use (global is GARTS, voice-local is LARTS)')
 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-samp-width', dest='samp_width', type='int', help='Set the width of the sample pane (by default display width / 2)')
 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-bgr-width', dest='bgr_width', type='int', help='Set the width of the bargraph pane (by default display width / 2)')
@@ -76,6 +77,10 @@ LAST_SYN = None
 CUR_PERIODS = [0] * STREAMS
 CUR_PERIODS = [0] * STREAMS
 CUR_PERIOD = 0.0
 CUR_PERIOD = 0.0
 
 
+GARTS = [0.0] * options.narts
+VLARTS = [[0.0] * options.narts for i in xrange(STREAMS)]
+LARTS = 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
 
 
@@ -493,7 +498,7 @@ else:
         return struct.pack(str(amt)+'i', *out)
         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, DRIFT_FACTOR, DRIFT_ERROR, CUR_PERIOD
+    global FREQS, PHASE, Z_SAMP, LAST_SAMP, LAST_SAMPLES, QUEUED_PCM, DRIFT_FACTOR, DRIFT_ERROR, CUR_PERIOD, LARTS
     if len(QUEUED_PCM) >= frames*4:
     if len(QUEUED_PCM) >= frames*4:
         desired_frames = DRIFT_FACTOR * frames
         desired_frames = DRIFT_FACTOR * frames
         err_frames = desired_frames - int(desired_frames)
         err_frames = desired_frames - int(desired_frames)
@@ -523,6 +528,7 @@ def gen_data(data, frames, tm, status):
         EXPIRATION = EXPIRATIONS[i]
         EXPIRATION = EXPIRATIONS[i]
         PHASE = PHASES[i]
         PHASE = PHASES[i]
         CUR_PERIOD = CUR_PERIODS[i]
         CUR_PERIOD = CUR_PERIODS[i]
+        LARTS = VLARTS[i]
         if FREQ != 0:
         if FREQ != 0:
             if time.time() > EXPIRATION:
             if time.time() > EXPIRATION:
                 FREQ = 0
                 FREQ = 0
@@ -652,5 +658,21 @@ while True:
                 DRIFT_FACTOR = 1.0 + float(bufnow - bufamt) / (bufamt * dfr * options.pcm_corr_rate)
                 DRIFT_FACTOR = 1.0 + float(bufnow - bufamt) / (bufamt * dfr * options.pcm_corr_rate)
                 print '\x1b[37m (DRIFT_FACTOR=%08.6f)'%(DRIFT_FACTOR,),
                 print '\x1b[37m (DRIFT_FACTOR=%08.6f)'%(DRIFT_FACTOR,),
             print
             print
+    elif pkt.cmd == CMD.ARTP:
+        print '\x1b[1;36mARTP',
+        if pkt.data[0] == OBLIGATE_POLYPHONE:
+            print '\x1b[1;31mGLOBAL',
+        else:
+            vrgb = [int(i*255) for i in colorsys.hls_to_rgb(float(pkt.data[0]) / STREAMS * 2.0 / 3.0, 0.5, 1.0)]
+            print '\x1b[1;38;2;{};{};{}mVOICE'.format(*vrgb), '{:03}'.format(pkt.data[0]),
+        print '\x1b[1;36mINDEX', pkt.data[1], '\x1b[1;37mVALUE', '%08.6f'%pkt.as_float(2),
+        if pkt.data[1] >= options.narts:
+            print '\x1b[1;31mOOB!!!',
+        else:
+            if pkt.data[0] == OBLIGATE_POLYPHONE:
+                GARTS[pkt.data[1]] = pkt.as_float(2)
+            else:
+                VLARTS[pkt.data[0]][pkt.data[1]] = pkt.as_float(2)
+        print
     else:
     else:
         print '\x1b[1;31mUnknown cmd', pkt.cmd
         print '\x1b[1;31mUnknown cmd', pkt.cmd

+ 70 - 9
mkiv.py

@@ -23,6 +23,8 @@ 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('-a', '--artp', dest='artp', action='append', help='Add articulation parameters to matching events (try --help-artp)')
+parser.add_option('--help-artp', dest='help_artp', action='store_true', help='Print help on articulation filters for events')
 parser.add_option('-p', '--program-split', dest='tracks', action='append_const', const=PROGRAMS, help='Ensure all programs are on non-mutual streams (overrides -T presently)')
 parser.add_option('-p', '--program-split', dest='tracks', action='append_const', const=PROGRAMS, help='Ensure all programs are on non-mutual streams (overrides -T presently)')
 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('-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)')
@@ -57,7 +59,7 @@ parser.add_option('--wav-log-width', dest='wav_log_width', type='float', help='W
 parser.add_option('--wav-log-base', dest='wav_log_base', type='float', help='Base of the logarithm used to scale low frequencies')
 parser.add_option('--wav-log-base', dest='wav_log_base', type='float', help='Base of the logarithm used to scale low frequencies')
 parser.add_option('--compression', dest='compression', help='Type of compression to use')
 parser.add_option('--compression', dest='compression', help='Type of compression to use')
 parser.add_option('--compressions', dest='compressions', action='store_true', help='List compressions that are supported')
 parser.add_option('--compressions', dest='compressions', action='store_true', help='List compressions that are supported')
-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, slack=0.0, real_slack=0.001, vol_pow=2, wav_winf='ones', wav_frames=512, wav_window=2048, wav_streams=16, wav_log_width=0.0, wav_log_base=2.0, compression='gzip')
+parser.set_defaults(tracks=[], artp=[], 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, slack=0.0, real_slack=0.001, vol_pow=2, wav_winf='ones', wav_frames=512, wav_window=2048, wav_streams=16, wav_log_width=0.0, wav_log_base=2.0, compression='gzip')
 options, args = parser.parse_args()
 options, args = parser.parse_args()
 if options.tempo == 'f1':
 if options.tempo == 'f1':
     options.tempo == 'global'
     options.tempo == 'global'
@@ -109,6 +111,22 @@ had been specified, again containing only the programs that were observed in the
 Groups for which no streams are generated are not written to the resulting file.'''
 Groups for which no streams are generated are not written to the resulting file.'''
     exit()
     exit()
 
 
+if options.help_artp:
+    print '''Articulation filters are used to attach articulations to various events.
+
+An articulation filter is a pair idx:expr, where idx is an integer and expr is a Python expression compiled as the tail of "lambda ev: ". `ev` is a DurationEvents possessing all the properties of MergeEvent (see --help-conds), as well as:
+
+- ev.duration: the duration, in seconds, of the note, as considered by the scheduler (including slack);
+- ev.real_duration: the duration, in seconds, that this note will play on a client;
+- ev.pitch: the MIDI pitch after resolving various modulation events (fractional);
+- ev.modwheel: the value of the MIDI modwheel at the time of this event;
+- ev.ampl: the amplitude (0.0-1.0) of this event.
+
+Each filter is applied in the order encountered on the command line. The expression may return None (in which case no parameter change is attached), or a float value, which is inserted before the event in the notestream, or a singleton of (float,), which will cause a global articulation (GARTS vs. LARTS--see client.py).
+
+This section is TODO.'''
+    exit()
+
 COMPRESSIONS = {}
 COMPRESSIONS = {}
 def compression(name, desc='Not described.'):
 def compression(name, desc='Not described.'):
     def inner(f):
     def inner(f):
@@ -784,6 +802,44 @@ for fname in args:
             else:
             else:
                 print 'ok'
                 print 'ok'
 
 
+    print 'Applying articulation parameters...'
+    class Articulation(object):
+        __slots__ = ['tm', 'index', 'value', 'global_']
+        def __init__(self, tm, index, value, global_=False):
+            self.tm = tm
+            self.index = index
+            self.value = value
+            self.global_ = global_
+
+    for artpex in options.artp:
+        cnt = 0
+        idx, _, artfex = artpex.partition(':')
+        idx = int(idx)
+        artf = eval('lambda ev: '+artfex)
+        for group in notegroups:
+            for ns in group.streams:
+                i = 0
+                while i < len(ns.history):
+                    ev = ns.history[i]
+                    if ev.__class__ is Articulation:
+                        i += 1
+                        continue
+                    val = artf(ev)
+                    if val is not None:
+                        global_ = False
+                        try:
+                            val = val[0]
+                        except TypeError:
+                            pass
+                        else:
+                            global_ = True
+                        ns.history.insert(i, Articulation(ev.abstime, idx, val, global_))
+                        i += 2
+                        cnt += 1
+                    else:
+                        i += 1
+        print 'Articulation parameter', idx, 'attached to', cnt, 'events'
+
     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'
 
 
@@ -812,14 +868,19 @@ for fname in args:
                     if group.name is not None:
                     if group.name is not None:
                             ivns.set('group', group.name)
                             ivns.set('group', group.name)
                     for note in ns.history:
                     for note in ns.history:
-                            ivnote = ET.SubElement(ivns, 'note', id=str(id(note)))
-                            ivnote.set('pitch', str(note.pitch))
-                            ivnote.set('vel', str(int(note.ampl * 127.0)))
-                            ivnote.set('ampl', str(note.ampl))
-                            ivnote.set('time', str(note.abstime))
-                            ivnote.set('dur', str(note.real_duration + options.real_slack))
-                            if note.par:
-                                ivnote.set('par', str(id(note.par)))
+                        if note.__class__ is Articulation:
+                            ivart = ET.SubElement(ivns, 'art', time=str(note.tm), index=str(note.index), value=str(note.value))
+                            if note.global_:
+                                ivart.set('global', '1')
+                            continue
+                        ivnote = ET.SubElement(ivns, 'note', id=str(id(note)))
+                        ivnote.set('pitch', str(note.pitch))
+                        ivnote.set('vel', str(int(note.ampl * 127.0)))
+                        ivnote.set('ampl', str(note.ampl))
+                        ivnote.set('time', str(note.abstime))
+                        ivnote.set('dur', str(note.real_duration + options.real_slack))
+                        if note.par:
+                            ivnote.set('par', str(id(note.par)))
 
 
     if not options.no_text:
     if not options.no_text:
         ivtext = ET.SubElement(ivstreams, 'stream', type='text')
         ivtext = ET.SubElement(ivstreams, 'stream', type='text')

+ 1 - 0
packet.py

@@ -26,6 +26,7 @@ class CMD:
         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
         PCMSYN = 6 # number of samples which should be buffered right now
+        ARTP = 7 # voice (or -1 = OBLIGATE_POLYPHONE for global), index, value(f32)
 
 
 class PLF:
 class PLF:
         SAMEPHASE = 0x1
         SAMEPHASE = 0x1