Ver Fonte

vibrato, chorus, and parent events

Graham Northup há 7 anos atrás
pai
commit
f93733a790
5 ficheiros alterados com 60 adições e 22 exclusões
  1. 9 2
      broadcast.py
  2. 21 4
      client.py
  3. 4 1
      drums.py
  4. 22 14
      mkiv.py
  5. 4 1
      packet.py

+ 9 - 2
broadcast.py

@@ -11,7 +11,7 @@ import itertools
 import re
 import os
 
-from packet import Packet, CMD, itos, OBLIGATE_POLYPHONE
+from packet import Packet, CMD, PLF, itos, OBLIGATE_POLYPHONE
 
 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)')
@@ -32,6 +32,7 @@ parser.add_option('-V', '--volume', dest='volume', type='float', help='Master vo
 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)')
+parser.add_option('-c', '--clamp', dest='clamp', action='store_true', help='Clamp over-the-wire amplitudes to 0.0-1.0')
 parser.add_option('-r', '--route', dest='routes', action='append', help='Add a routing directive (see --route-help)')
 parser.add_option('--clear-routes', dest='routes', action='store_const', const=[], help='Clear routes previously specified (including the default)')
 parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Be verbose; dump events and actual time (can slow down performance!)')
@@ -609,8 +610,14 @@ for fname in args:
                     if options.dry:
                         playing_notes[self.nsid] = (pitch, ampl)
                     else:
+                        amp = ampl * options.volume
+                        if options.clamp:
+                            amp = max(min(amp, 1.0), 0.0)
+                        flags = 0
+                        if note.get('parent', None):
+                            flags |= PLF.SAMEPHASE
                         for cl in cls:
-                            s.sendto(str(Packet(CMD.PLAY, int(pl_dur), int((pl_dur*1000000)%1000000), int(440.0 * 2**((pitch-69)/12.0)), ampl * options.volume, cl[2])), cl[:2])
+                            s.sendto(str(Packet(CMD.PLAY, int(pl_dur), int((pl_dur*1000000)%1000000), int(440.0 * 2**((pitch-69)/12.0)), amp, cl[2], flags)), cl[:2])
                             playing_notes[cl] = (pitch, ampl)
                 if i > 0 and dur is not None:
                     self.cur_offt = ttime + dur / options.factor

+ 21 - 4
client.py

@@ -15,7 +15,7 @@ import threading
 import thread
 import colorsys
 
-from packet import Packet, CMD, stoi
+from packet import Packet, CMD, PLF, 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')
@@ -28,6 +28,10 @@ parser.add_option('-V', '--volume', dest='volume', type='float', default=1.0, he
 parser.add_option('-n', '--streams', dest='streams', type='int', default=1, help='Set the number of streams this client will play back')
 parser.add_option('-N', '--numpy', dest='numpy', action='store_true', help='Use numpy acceleration')
 parser.add_option('-G', '--gui', dest='gui', default='', help='set a GUI to use')
+parser.add_option('-c', '--clamp', dest='clamp', action='store_true', help='Clamp over-the-wire amplitudes to 0.0-1.0')
+parser.add_option('-C', '--chorus', dest='chorus', default=0.0, type='float', help='Apply uniform random offsets (in MIDI pitch space)')
+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('--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)')
@@ -483,6 +487,10 @@ def gen_data(data, frames, tm, status):
         fdata = [0] * frames
     for i in range(STREAMS):
         FREQ = FREQS[i]
+        if options.vibrato > 0 and FREQ > 0:
+            midi = 12 * math.log(FREQ / 440.0, 2) + 69
+            midi += options.vibrato * math.sin(time.time() * 2 * math.pi * options.vibrato_freq + i * 2 * math.pi / STREAMS)
+            FREQ = 440.0 * 2 ** ((midi - 69) / 12)
         LAST_SAMP = LAST_SAMPS[i]
         AMP = AMPS[i]
         EXPIRATION = EXPIRATIONS[i]
@@ -492,7 +500,6 @@ def gen_data(data, frames, tm, status):
                 FREQ = 0
                 FREQS[i] = 0
         if FREQ == 0:
-            PHASES[i] = 0
             if LAST_SAMP != 0:
                 vdata = lin_seq(LAST_SAMP, 0, frames)
                 fdata = mix(fdata, vdata)
@@ -558,9 +565,19 @@ while True:
     elif pkt.cmd == CMD.PLAY:
         voice = pkt.data[4]
         dur = pkt.data[0]+pkt.data[1]/1000000.0
-        FREQS[voice] = pkt.data[2]
-        AMPS[voice] = MAX * max(min(pkt.as_float(3), 1.0), 0.0)
+        freq = pkt.data[2]
+        if options.chorus > 0:
+            midi = 12 * math.log(freq / 440.0, 2) + 69
+            midi += (random.random() * 2 - 1) * options.chorus
+            freq = 440.0 * 2 ** ((midi - 69) / 12)
+        FREQS[voice] = freq
+        amp = pkt.as_float(3)
+        if options.clamp:
+            amp = max(min(amp, 1.0), 0.0)
+        AMPS[voice] = MAX * amp
         EXPIRATIONS[voice] = time.time() + dur
+        if not (pkt.data[5] & PLF.SAMEPHASE):
+            PHASES[voice] = 0.0
         vrgb = [int(i*255) for i in colorsys.hls_to_rgb(float(voice) / STREAMS * 2.0 / 3.0, 0.5, 1.0)]
         frgb = rgb_for_freq_amp(pkt.data[2], pkt.as_float(3))
         print '\x1b[1;32mPLAY',

+ 4 - 1
drums.py

@@ -17,6 +17,7 @@ parser.add_option('-V', '--volume', dest='volume', type='float', default=1.0, he
 parser.add_option('-r', '--rate', dest='rate', type='int', default=44100, help='Audio sample rate for output and of input files')
 parser.add_option('-u', '--uid', dest='uid', default='', help='User identifier of this client')
 parser.add_option('-p', '--port', dest='port', default=13677, type='int', help='UDP port to listen on')
+parser.add_option('-c', '--clamp', dest='clamp', action='store_true', help='Clamp over-the-wire amplitudes to 0.0-1.0')
 parser.add_option('--repeat', dest='repeat', action='store_true', help='If a note plays longer than a sample length, keep playing the sample')
 parser.add_option('--cut', dest='cut', action='store_true', help='If a note ends within a sample, stop playing that sample immediately')
 parser.add_option('-n', '--max-voices', dest='max_voices', default=-1, type='int', help='Only support this many notes playing simultaneously (earlier ones get dropped)')
@@ -184,7 +185,9 @@ while True:
             dframes = max(dframes, rframes)
         if not options.cut:
             dframes = rframes * ((dframes + rframes - 1) / rframes)
-        amp = max(min(options.volume * pkt.as_float(3), 1.0), 0.0)
+        amp = options.volume * pkt.as_float(3)
+        if options.clamp:
+            amp = max(min(amp, 1.0), 0.0)
         PLAYING.append(SampleReader(rdata, dframes * 4, amp))
         if options.max_voices >= 0:
             while len(PLAYING) > options.max_voices:

+ 22 - 14
mkiv.py

@@ -166,7 +166,7 @@ for fname in args:
         print fname, ': Too fucked to continue'
         continue
     iv = ET.Element('iv')
-    iv.set('version', '1')
+    iv.set('version', '1.1')
     iv.set('src', os.path.basename(fname))
     print fname, ': MIDI format,', len(pat), 'tracks'
     if options.verbose:
@@ -364,39 +364,43 @@ for fname in args:
     print 'Generating streams...'
 
     class DurationEvent(MergeEvent):
-        __slots__ = ['duration', 'real_duration', 'pitch', 'modwheel', 'ampl']
-        def __init__(self, me, pitch, ampl, dur, modwheel=0):
+        __slots__ = ['duration', 'real_duration', 'pitch', 'modwheel', 'ampl', 'parent']
+        def __init__(self, me, pitch, ampl, dur, modwheel=0, parent=None):
             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.real_duration = dur
             self.modwheel = modwheel
+            self.parent = parent
 
         def __repr__(self):
             return '<NE %s P:%f A:%f D:%f W:%f>'%(MergeEvent.__repr__(self), self.pitch, self.ampl, self.duration, self.modwheel)
 
     class NoteStream(object):
-        __slots__ = ['history', 'active', 'bentpitch', 'modwheel']
+        __slots__ = ['history', 'active', 'bentpitch', 'modwheel', 'prevparent']
         def __init__(self):
             self.history = []
             self.active = None
             self.bentpitch = None
             self.modwheel = 0
+            self.prevparent = None
         def IsActive(self):
             return self.active is not None
-        def Activate(self, mev, bentpitch=None, modwheel=None):
+        def Activate(self, mev, bentpitch=None, modwheel=None, parent=None):
             if bentpitch is None:
                 bentpitch = mev.ev.pitch
             self.active = mev
             self.bentpitch = bentpitch
             if modwheel is not None:
                 self.modwheel = modwheel
+            self.prevparent = parent
         def Deactivate(self, mev):
-            self.history.append(DurationEvent(self.active, self.bentpitch, self.active.ev.velocity / 127.0, mev.abstime - self.active.abstime, self.modwheel))
+            self.history.append(DurationEvent(self.active, self.bentpitch, self.active.ev.velocity / 127.0, mev.abstime - self.active.abstime, self.modwheel, self.prevparent))
             self.active = None
             self.bentpitch = None
             self.modwheel = 0
+            self.prevparent = None
         def WouldDeactivate(self, mev):
             if not self.IsActive():
                 return False
@@ -492,9 +496,10 @@ for fname in args:
             for group in notegroups:
                 for stream in group.streams:
                     if stream.WouldDeactivate(mev):
-                        base = stream.active.copy(abstime=mev.abstime)
+                        old = stream.active
+                        base = old.copy(abstime=mev.abstime)
                         stream.Deactivate(mev)
-                        stream.Activate(base, base.ev.pitch + options.deviation * (mev.ev.pitch / float(0x2000)))
+                        stream.Activate(base, base.ev.pitch + options.deviation * (mev.ev.pitch / float(0x2000)), parent=old)
                         found = True
             if not found:
                 print 'WARNING: Did not find any matching active streams for %r'%(mev,)
@@ -509,9 +514,10 @@ for fname in args:
             for group in notegroups:
                 for stream in group.streams:
                     if stream.WouldDeactivate(mev):
-                        base = stream.active.copy(abstime=mev.abstime)
+                        old = stream.active
+                        base = old.copy(abstime=mev.abstime)
                         stream.Deactivate(mev)
-                        stream.Activate(base, stream.bentpitch, mev.mw)
+                        stream.Activate(base, stream.bentpitch, mev.mw, parent=old)
                         found = True
             if not found:
                 print 'WARNING: Did not find any matching active streams for %r'%(mev,)
@@ -582,7 +588,7 @@ for fname in args:
                                 t = origtime
                             else:
                                 t = dt
-                            events.append(DurationEvent(dev, realpitch + mwamp * options.modfdev * math.sin(2 * math.pi * options.modffreq * t), realamp + mwamp * options.modadev * (math.sin(2 * math.pi * options.modafreq * t) - 1.0) / 2.0, min(options.modres, dev.duration - dt), dev.modwheel))
+                            events.append(DurationEvent(dev, realpitch + mwamp * options.modfdev * math.sin(2 * math.pi * options.modffreq * t), realamp + mwamp * options.modadev * (math.sin(2 * math.pi * options.modafreq * t) - 1.0) / 2.0, min(options.modres, dev.duration - dt), dev.modwheel, dev))
                             dt += options.modres
                         ns.history[i:i+1] = events
                         i += len(events)
@@ -617,7 +623,7 @@ for fname in args:
                     events = []
                     while dt < dev.duration and ampf * dev.ampl >= options.stringthres:
                         dev.abstime = origtime + dt
-                        events.append(DurationEvent(dev, dev.pitch, ampf * dev.ampl, min(options.stringres, dev.duration - dt), dev.modwheel))
+                        events.append(DurationEvent(dev, dev.pitch, ampf * dev.ampl, min(options.stringres, dev.duration - dt), dev.modwheel, dev))
                         if len(events) > options.stringmax:
                             print 'WARNING: Exceeded maximum string model events for event', i
                             if options.verbose:
@@ -629,7 +635,7 @@ for fname in args:
                     dt = dev.duration
                     while ampf * dev.ampl >= options.stringthres:
                         dev.abstime = origtime + dt
-                        events.append(DurationEvent(dev, dev.pitch, ampf * dev.ampl, options.stringres, dev.modwheel))
+                        events.append(DurationEvent(dev, dev.pitch, ampf * dev.ampl, options.stringres, dev.modwheel, dev))
                         if len(events) > options.stringmax:
                             print 'WARNING: Exceeded maximum string model events for event', i
                             if options.verbose:
@@ -771,12 +777,14 @@ 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')
+                            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))
+                            if note.parent:
+                                ivnote.set('parent', str(id(note.parent)))
 
     if not options.no_text:
         ivtext = ET.SubElement(ivstreams, 'stream', type='text')

+ 4 - 1
packet.py

@@ -22,11 +22,14 @@ 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.0 - 1.0), port
+	PLAY = 3 # seconds, microseconds, frequency (Hz), amplitude (0.0 - 1.0), port, flags
         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
 
+class PLF:
+        SAMEPHASE = 0x1
+
 def itos(i):
     return struct.pack('>L', i).rstrip('\0')