Ver Fonte

Adding articulation parameters

Graham Northup há 7 anos atrás
pai
commit
fac3853e5f
4 ficheiros alterados com 117 adições e 13 exclusões
  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
                     note = nsq.pop(0)
                     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
                     ampl = float(note.get('ampl', float(note.get('vel', 127.0)) / 127.0))
                     dur = factor*float(note.get('dur'))
@@ -666,6 +676,16 @@ for fname in args:
                     nsq, cls = self._Thread__args
                     for note in nsq:
                             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
                             ampl = float(note.get('ampl', float(note.get('vel', 127.0)) / 127.0))
                             dur = factor*float(note.get('dur'))
@@ -697,7 +717,7 @@ for fname in args:
         for idx, ns in zip(xrange(number), nscycle):
             clis = routeset.Route(ns)
             for cli in clis:
-                nsq = ns.findall('note')
+                nsq = ns.findall('*')
                 nsq.sort(key=lambda x: float(x.get('time')))
                 if ns in threads:
                     threads[ns]._Thread__args[1].add(cli)
@@ -710,7 +730,7 @@ for fname in args:
             print thr._Thread__args[1]
 
     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
     if options.seek > 0:
         for thr in threads.values():

+ 24 - 2
client.py

@@ -15,7 +15,7 @@ import threading
 import thread
 import colorsys
 
-from packet import Packet, CMD, PLF, stoi
+from packet import Packet, CMD, PLF, stoi, OBLIGATE_POLYPHONE
 
 parser = optparse.OptionParser()
 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-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('--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-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)')
@@ -76,6 +77,10 @@ LAST_SYN = None
 CUR_PERIODS = [0] * STREAMS
 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):
     return p*to + (1-p)*frm
 
@@ -493,7 +498,7 @@ else:
         return struct.pack(str(amt)+'i', *out)
 
 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:
         desired_frames = DRIFT_FACTOR * frames
         err_frames = desired_frames - int(desired_frames)
@@ -523,6 +528,7 @@ def gen_data(data, frames, tm, status):
         EXPIRATION = EXPIRATIONS[i]
         PHASE = PHASES[i]
         CUR_PERIOD = CUR_PERIODS[i]
+        LARTS = VLARTS[i]
         if FREQ != 0:
             if time.time() > EXPIRATION:
                 FREQ = 0
@@ -652,5 +658,21 @@ while True:
                 DRIFT_FACTOR = 1.0 + float(bufnow - bufamt) / (bufamt * dfr * options.pcm_corr_rate)
                 print '\x1b[37m (DRIFT_FACTOR=%08.6f)'%(DRIFT_FACTOR,),
             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:
         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', 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('-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', '--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)')
@@ -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('--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.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()
 if options.tempo == 'f1':
     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.'''
     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 = {}
 def compression(name, desc='Not described.'):
     def inner(f):
@@ -784,6 +802,44 @@ for fname in args:
             else:
                 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 'Playtime:', lastabstime, 'seconds'
 
@@ -812,14 +868,19 @@ for fname in args:
                     if group.name is not None:
                             ivns.set('group', group.name)
                     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:
         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)
         PCM = 5 # 16 samples, encoded S16_LE
         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:
         SAMEPHASE = 0x1