Parcourir la source

Modwheel stuff, floating-point amplitude

Grissess il y a 9 ans
Parent
commit
bb38c09530
4 fichiers modifiés avec 113 ajouts et 34 suppressions
  1. 10 10
      broadcast.py
  2. 1 1
      client.py
  3. 98 21
      mkiv.py
  4. 4 2
      packet.py

+ 10 - 10
broadcast.py

@@ -26,7 +26,7 @@ parser.add_option('-q', '--quit', dest='quit', action='store_true', help='Instru
 parser.add_option('-p', '--play', dest='play', action='append', help='Play a single tone or chord (specified multiple times) on all listening clients (either "midi pitch" or "@frequency")')
 parser.add_option('-P', '--play-async', dest='play_async', action='store_true', help='Don\'t wait for the tone to finish using the local clock')
 parser.add_option('-D', '--duration', dest='duration', type='float', help='How long to play this note for')
-parser.add_option('-V', '--volume', dest='volume', type='int', help='Master volume (0-255)')
+parser.add_option('-V', '--volume', dest='volume', type='float', help='Master volume [0.0, 1.0]')
 parser.add_option('-s', '--silence', dest='silence', action='store_true', help='Instruct all clients to stop playing any active tones')
 parser.add_option('-S', '--seek', dest='seek', type='float', help='Start time in seconds (scaled by --factor)')
 parser.add_option('-f', '--factor', dest='factor', type='float', help='Rescale time by this factor (0<f<1 are faster; 0.5 is twice the speed, 2 is half)')
@@ -41,7 +41,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=255, wait_time=0.25, play=[], transpose=0, seek=0.0, bind_addr='', pg_width = 0, pg_height = 0, number=-1)
+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.25, play=[], transpose=0, seek=0.0, bind_addr='', pg_width = 0, pg_height = 0, number=-1)
 options, args = parser.parse_args()
 
 if options.help_routes:
@@ -103,7 +103,7 @@ def gui_pygame():
         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 = colorsys.hls_to_rgb(float(idx) / len(clients), note[1]/2.0, 1.0)
             col = [int(i*255) for i in col]
             disp.fill(col, (WIDTH - 1, HEIGHT - pitch * PFAC - PFAC, 1, PFAC))
             idx += 1
@@ -177,7 +177,7 @@ for cl in clients:
 	if options.quit:
 		s.sendto(str(Packet(CMD.QUIT)), cl)
         if options.silence:
-                s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0)), cl)
+                s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0.0)), cl)
 
 if options.gui:
     gui_thr = threading.Thread(target=GUIS[options.gui], args=())
@@ -199,7 +199,7 @@ if options.play:
 if options.test and options.sync_test:
     time.sleep(0.25)
     for cl in clients:
-        s.sendto(str(Packet(CMD.PLAY, 0, 250000, 880, 255)), cl)
+        s.sendto(str(Packet(CMD.PLAY, 0, 250000, 880, 1.0)), cl)
 
 if options.test or options.quit or options.silence:
     print uid_groups
@@ -270,9 +270,9 @@ if options.live or options.list_live:
                     print 'WARNING: Out of clients to do note %r; dropped'%(event.pitch,)
                     continue
                 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)), event.velocity / 127.0)), cli)
                 active_set.setdefault(event.pitch, []).append(cli)
-                playing_notes[cli] = (event.pitch, 2*event.velocity)
+                playing_notes[cli] = (event.pitch, event.velocity / 127.0)
                 if options.verbose:
                     print 'LIVE:', event.pitch, '+ =>', active_set[event.pitch]
             elif isinstance(event, midi.NoteOffEvent):
@@ -473,15 +473,15 @@ for fname in args:
                     for note in nsq:
                             ttime = float(note.get('time'))
                             pitch = float(note.get('pitch')) + options.transpose
-                            vel = int(note.get('vel'))
+                            ampl = float(note.get('ampl', note.get('vel', 127.0) / 127.0))
                             dur = factor*float(note.get('dur'))
                             while time.time() - BASETIME < factor*ttime:
                                     self.wait_for(factor*ttime - (time.time() - BASETIME))
                             for cl in cls:
-                                    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)), ampl * options.volume)), cl)
                             if options.verbose:
                                 print (time.time() - BASETIME), cl, ': PLAY', pitch, dur, vel
-                            playing_notes[cl] = (pitch, vel*2)
+                            playing_notes[cl] = (pitch, ampl)
                             self.wait_for(dur - ((time.time() - BASETIME) - factor*ttime))
                             playing_notes[cl] = (0, 0)
                     if options.verbose:

+ 1 - 1
client.py

@@ -355,7 +355,7 @@ while True:
     elif pkt.cmd == CMD.PLAY:
         dur = pkt.data[0]+pkt.data[1]/1000000.0
         FREQ = pkt.data[2]
-        AMP = MAX * (pkt.data[3]/255.0)
+        AMP = MAX * (pkt.as_float(3))
         signal.setitimer(signal.ITIMER_REAL, dur)
     elif pkt.cmd == CMD.CAPS:
         data = [0] * 8

+ 98 - 21
mkiv.py

@@ -32,8 +32,14 @@ parser.add_option('-f', '--fuckit', dest='fuckit', action='store_true', help='Us
 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 (please use less or write to a file)')
 parser.add_option('-D', '--deviation', dest='deviation', type='int', help='Amount (in semitones/MIDI pitch units) by which a fully deflected pitchbend modifies the base pitch (0 disables pitchbend processing)')
+parser.add_option('-M', '--modwheel-freq-dev', dest='modfdev', type='float', help='Amount (in semitones/MIDI pitch unites) by which a fully-activated modwheel modifies the base pitch')
+parser.add_option('--modwheel-freq-freq', dest='modffreq', type='float', help='Frequency of modulation periods (sinusoids) of the modwheel acting on the base pitch')
+parser.add_option('--modwheel-amp-dev', dest='modadev', type='float', help='Deviation [0, 1] by which a fully-activated modwheel affects the amplitude as a factor of that amplitude')
+parser.add_option('--modwheel-amp-freq', dest='modafreq', type='float', help='Frequency of modulation periods (sinusoids) of the modwheel acting on amplitude')
+parser.add_option('--modwheel-res', dest='modres', type='float', help='(Fractional) seconds by which to resolve modwheel events (0 to disable)')
 parser.add_option('--tempo', dest='tempo', help='Adjust interpretation of tempo (try "f1"/"global", "f2"/"track")')
-parser.set_defaults(tracks=[], perc='GM', deviation=2, tempo='global')
+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.01, modfdev=1.0, modffreq=5.0, modadev=0.5, modafreq=5.0)
 options, args = parser.parse_args()
 if options.tempo == 'f1':
     options.tempo == 'global'
@@ -49,6 +55,7 @@ The "ev" object will be a MergeEvent with the following properties:
 -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.mw: the modwheel value
 -ev.ev: a midi.NoteOnEvent:
     -ev.ev.pitch: the MIDI pitch
     -ev.ev.velocity: the MIDI velocity
@@ -214,23 +221,26 @@ for fname in args:
         return rt
 
     class MergeEvent(object):
-        __slots__ = ['ev', 'tidx', 'abstime', 'bank', 'prog']
-        def __init__(self, ev, tidx, abstime, bank, prog):
+        __slots__ = ['ev', 'tidx', 'abstime', 'bank', 'prog', 'mw']
+        def __init__(self, ev, tidx, abstime, bank=0, prog=0, mw=0):
             self.ev = ev
             self.tidx = tidx
             self.abstime = abstime
             self.bank = bank
             self.prog = prog
+            self.mw = mw
         def copy(self, **kwargs):
-            args = {'ev': self.ev, 'tidx': self.tidx, 'abstime': self.abstime, 'bank': self.bank, 'prog': self.prog}
+            args = {'ev': self.ev, 'tidx': self.tidx, 'abstime': self.abstime, 'bank': self.bank, 'prog': self.prog, 'mw': self.mw}
             args.update(kwargs)
             return MergeEvent(**args)
         def __repr__(self):
-            return '<ME %r in %d on (%d:%d) @%f>'%(self.ev, self.tidx, self.bank, self.prog, self.abstime)
+            return '<ME %r in %d on (%d:%d) MW:%d @%f>'%(self.ev, self.tidx, self.bank, self.prog, self.mw, self.abstime)
 
     events = []
+    cur_mw = [[0 for i in range(16)] for j in range(len(pat))]
     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_mw = [[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))]
@@ -253,18 +263,26 @@ for fname in args:
                 progs.add(ev.value)
                 chg_prog[tidx][ev.channel] += 1
             elif isinstance(ev, midi.ControlChangeEvent):
-                if ev.control == 0:
+                if ev.control == 0:  # Bank -- MSB
                     cur_bank[tidx][ev.channel] = (0x3F80 & cur_bank[tidx][ev.channel]) | ev.value
                     chg_bank[tidx][ev.channel] += 1
-                elif ev.control == 32:
+                elif ev.control == 32:  # Bank -- LSB
                     cur_bank[tidx][ev.channel] = (0x3F & cur_bank[tidx][ev.channel]) | (ev.value << 7)
                     chg_bank[tidx][ev.channel] += 1
+                elif ev.control == 1:  # ModWheel -- MSB
+                    cur_mw[tidx][ev.channel] = (0x3F80 & cur_mw[tidx][ev.channel]) | ev.value
+                    chg_mw[tidx][ev.channel] += 1
+                elif ev.control == 33:  # ModWheel -- LSB
+                    cur_mw[tidx][ev.channel] = (0x3F & cur_mw[tidx][ev.channel]) | (ev.value << 7)
+                    chg_mw[tidx][ev.channel] += 1
+                events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel], cur_mw[tidx][ev.channel]))
+                ev_cnts[tidx][ev.channel] += 1
             elif isinstance(ev, midi.MetaEventWithText):
-                events.append(MergeEvent(ev, tidx, abstime, 0, 0))
+                events.append(MergeEvent(ev, tidx, abstime))
             elif isinstance(ev, midi.Event):
                 if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0:
                     ev.__class__ = midi.NoteOffEvent #XXX Oww
-                events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel]))
+                events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel], cur_mw[tidx][ev.channel]))
                 ev_cnts[tidx][ev.channel] += 1
 
     if options.verbose:
@@ -281,27 +299,32 @@ for fname in args:
     print 'Generating streams...'
 
     class DurationEvent(MergeEvent):
-        __slots__ = ['duration', 'pitch']
-        def __init__(self, me, pitch, dur):
-            MergeEvent.__init__(self, me.ev, me.tidx, me.abstime, me.bank, me.prog)
+        __slots__ = ['duration', 'pitch', 'modwheel', 'ampl']
+        def __init__(self, me, pitch, ampl, dur, modwheel=0):
+            MergeEvent.__init__(self, me.ev, me.tidx, me.abstime, me.bank, me.prog, me.mw)
             self.pitch = pitch
+            self.ampl = ampl
             self.duration = dur
+            self.modwheel = modwheel
 
     class NoteStream(object):
-        __slots__ = ['history', 'active', 'realpitch']
+        __slots__ = ['history', 'active', 'bentpitch', 'modwheel']
         def __init__(self):
             self.history = []
             self.active = None
-            self.realpitch = None
+            self.bentpitch = None
+            self.modwheel = 0
         def IsActive(self):
             return self.active is not None
-        def Activate(self, mev, realpitch = None):
-            if realpitch is None:
-                realpitch = mev.ev.pitch
+        def Activate(self, mev, bentpitch = None, modwheel = None):
+            if bentpitch is None:
+                bentpitch = mev.ev.pitch
             self.active = mev
-            self.realpitch = realpitch
+            self.bentpitch = bentpitch
+            if modwheel is not None:
+                self.modwheel = modwheel
         def Deactivate(self, mev):
-            self.history.append(DurationEvent(self.active, self.realpitch, mev.abstime - self.active.abstime))
+            self.history.append(DurationEvent(self.active, self.realpitch, self.active.ev.velocity / 127.0, .abstime - self.active.abstime, self.modwheel))
             self.active = None
             self.realpitch = None
         def WouldDeactivate(self, mev):
@@ -311,6 +334,8 @@ for fname in args:
                 return mev.ev.pitch == self.active.ev.pitch and mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel
             if isinstance(mev.ev, midi.PitchWheelEvent):
                 return mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel
+            if isinstance(mev.ev, midi.ControlChangeEvent):
+                return mev.tidx == self.active.tidx and mev.ev.channel = self.active.ev.channel
             raise TypeError('Tried to deactivate with bad type %r'%(type(mev.ev),))
 
     class NSGroup(object):
@@ -409,6 +434,23 @@ for fname in args:
                         print '    Group %r:'%(group.name,)
                         for stream in group.streams:
                             print '      Stream: %r'%(stream.active,)
+        elif options.modres <= 0 and isinstance(mev.ev, midi.ControlChangeEvent):
+            found = False
+            for group in notegroups:
+                for stream in group.streams:
+                    if stream.WouldDeactivate(mev):
+                        base = stream.active.copy(abstime=mev.abstime)
+                        stream.Deactivate(mev)
+                        stream.Activate(base, stream.bentpitch, mev.mw)
+                        found = True
+            if not found:
+                print 'WARNING: Did not find any matching active streams for %r'%(mev,)
+                if options.verbose:
+                    print '  Current state:'
+                    for group in notegroups:
+                        print '    Group %r:'%(group.name,)
+                        for stream in group.streams:
+                            print '      Stream: %r'%(stream.active,)
         else:
             auxstream.append(mev)
 
@@ -418,7 +460,41 @@ for fname in args:
         for ns in group.streams:
             if ns.IsActive():
                 print 'WARNING: Active notes at end of playback.'
-                ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime, 0, 0))
+                ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime))
+
+    if options.modres > 0:
+        print 'Resolving modwheel events...'
+        ev_cnt = 0
+        for group in notegroups:
+            for ns in group.streams:
+                i = 0
+                while i < len(ns.history):
+                    dev = ns.history[i]
+                    if dev.modwheel > 0:
+                        realpitch = dev.pitch
+                        realamp = dev.ampl
+                        dt = 0.0
+                        events = []
+                        while dt < dev.duration:
+                            events.append(DurationEvent(dev, realpitch + options.modfdev * math.sin(options.modffreq * (dev.abstime + dt)), realamp + options.modadev * math.sin(options.modafreq * (dev.abstime + dt)), dev.duration, dev.modwheel))
+                            dt += options.modres
+                        ns.history[i:i+1] = events
+                        i += len(events)
+                        ev_cnt += len(events)
+                    else:
+                        i += 1
+        print '...resolved', ev_cnt, 'events'
+
+    if not options.keepempty:
+        print 'Culling empty events...'
+        for group in notegroups:
+            for ns in group.streams:
+                i = 0
+                while i < len(ns.history):
+                    if ns.history[i].duration == 0.0:
+                        del ns.history[i]
+                    else:
+                        i += 1
 
     if options.verbose:
         print 'Final group mappings:'
@@ -455,7 +531,8 @@ for fname in args:
                     for note in ns.history:
                             ivnote = ET.SubElement(ivns, 'note')
                             ivnote.set('pitch', str(note.pitch))
-                            ivnote.set('vel', str(note.ev.velocity))
+                            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.duration))
 

+ 4 - 2
packet.py

@@ -13,14 +13,16 @@ class Packet(object):
         def FromStr(cls, s):
             parts = struct.unpack('>9L', s)
             return cls(parts[0], *parts[1:])
+        def as_float(self, i):
+            return struct.unpack('>f', struct.pack('>L', self.data[i]))[0]
 	def __str__(self):
-		return struct.pack('>L'+('L'*len(self.data)), self.cmd, *self.data)
+		return struct.pack('>L'+(''.join('f' if isinstance(i, float) else 'L' for i in self.data)), self.cmd, *self.data)
 
 class CMD:
 	KA = 0 # No important data
 	PING = 1 # Data are echoed exactly
 	QUIT = 2 # No important data
-	PLAY = 3 # seconds, microseconds, frequency (Hz), amplitude (0-255), port
+	PLAY = 3 # seconds, microseconds, frequency (Hz), amplitude (0.0 - 1.0), port
         CAPS = 4 # ports, client type (1), user ident (2-7)
 
 def itos(i):