aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--broadcast.py353
-rw-r--r--client.py239
-rw-r--r--downsamp.py117
-rw-r--r--drums.py215
-rw-r--r--drums.tar.bz2bin0 -> 3275151 bytes
-rwxr-xr-xmake_patfile.sh29
-rw-r--r--mkiv.py347
-rw-r--r--mktune.py63
-rw-r--r--packet.py13
-rw-r--r--shiv.py91
10 files changed, 1252 insertions, 215 deletions
diff --git a/broadcast.py b/broadcast.py
index c7d379d..1efbda3 100644
--- a/broadcast.py
+++ b/broadcast.py
@@ -8,14 +8,16 @@ import thread
import optparse
import random
import itertools
+import re
+import os
-from packet import Packet, CMD, itos
+from packet import Packet, CMD, 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)')
-parser.add_option('--test-delay', dest='test_delay', type='float', help='Time for which to play a test tone')
parser.add_option('-T', '--transpose', dest='transpose', type='int', help='Transpose by a set amount of semitones (positive or negative)')
parser.add_option('--sync-test', dest='sync_test', action='store_true', help='Don\'t wait for clients to play tones properly--have them all test tone at the same time')
+parser.add_option('--wait-test', dest='wait_test', action='store_true', help='Wait for user input before moving to the next client tested')
parser.add_option('-R', '--random', dest='random', type='float', help='Generate random notes at approximately this period')
parser.add_option('--rand-low', dest='rand_low', type='int', help='Low frequency to randomly sample')
parser.add_option('--rand-high', dest='rand_high', type='int', help='High frequency to randomly sample')
@@ -26,22 +28,30 @@ 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)')
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!)')
-parser.add_option('-W', '--wait-time', dest='wait_time', type='float', help='How long to wait for clients to initially respond (delays all broadcasts)')
+parser.add_option('-W', '--wait-time', dest='wait_time', type='float', help='How long to wait between pings for clients to initially respond (delays all broadcasts)')
+parser.add_option('--tries', dest='tries', type='int', help='Number of ping packets to send')
parser.add_option('-B', '--bind-addr', dest='bind_addr', help='The IP address (or IP:port) to bind to (influences the network to send to)')
+parser.add_option('--port', dest='ports', action='append', type='int', help='Add a port to find clients on')
+parser.add_option('--clear-ports', dest='ports', action='store_const', const=[], help='Clear ports previously specified (including the default)')
parser.add_option('--repeat', dest='repeat', action='store_true', help='Repeat the file playlist indefinitely')
parser.add_option('-n', '--number', dest='number', type='int', help='Number of clients to use; if negative (default -1), use the product of stream count and the absolute value of this parameter')
+parser.add_option('--dry', dest='dry', action='store_true', help='Dry run--don\'t actually search for or play to clients, but pretend they exist (useful with -G)')
+parser.add_option('--pcm', dest='pcm', action='store_true', help='Use experimental PCM rendering')
+parser.add_option('--pcm-lead', dest='pcmlead', type='float', help='Seconds of leading PCM data to send')
+parser.add_option('--spin', dest='spin', action='store_true', help='Ignore delta times in the queue (busy loop the CPU) for higher accuracy')
parser.add_option('-G', '--gui', dest='gui', default='', help='set a GUI to use')
parser.add_option('--pg-fullscreen', dest='fullscreen', action='store_true', help='Use a full-screen video mode')
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=['T:DRUM=!perc,0'], random=0.0, rand_low=80, rand_high=2000, live=None, factor=1.0, duration=0.25, volume=1.0, wait_time=0.1, tries=5, play=[], transpose=0, seek=0.0, bind_addr='', ports=[13676, 13677], pg_width = 0, pg_height = 0, number=-1, pcmlead=0.1)
options, args = parser.parse_args()
if options.help_routes:
@@ -50,8 +60,13 @@ if options.help_routes:
Routes are fully specified by:
-The attribute to be routed on (either type "T", or UID "U")
-The value of that attribute
--The exclusivity of that route ("+" for inclusive, "-" for exclusive)
--The stream group to be routed there.
+-The exclusivity of that route ("+" for inclusive, "-" for exclusive, "!" for complete)
+-The stream group to be routed there, or 0 to null route.
+The first two may be replaced by a single '0' to null route a stream--effective only when used with an exclusive route.
+
+"Complete" exclusivity is valid only for obligate polyphones, and indicates that *all* matches are to receive the stream. In other cases, this will have the undesirable effect of routing only one stream.
+
+The special group ALL matches all streams. Regular expressions may be used to specify groups. Note that the first character is *not* part of the regular expression.
The syntax for that specification resembles the following:
@@ -61,6 +76,7 @@ The specifier consists of a comma-separated list of attribute-colon-value pairs,
exit()
GUIS = {}
+BASETIME = time.time() # XXX fixes a race with the GUI
def gui_pygame():
print 'Starting pygame GUI...'
@@ -93,6 +109,7 @@ def gui_pygame():
PFAC = HEIGHT / 128.0
clock = pygame.time.Clock()
+ font = pygame.font.SysFont(pygame.font.get_default_font(), 24)
print 'Pygame GUI initialized, running...'
@@ -103,10 +120,13 @@ 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(targets), 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
+ tsurf = font.render('%0.3f' % ((time.time() - BASETIME) / factor,), True, (255, 255, 255), (0, 0, 0))
+ disp.fill((0, 0, 0), tsurf.get_rect())
+ disp.blit(tsurf, (0, 0))
pygame.display.flip()
for ev in pygame.event.get():
@@ -120,11 +140,17 @@ def gui_pygame():
GUIS['pygame'] = gui_pygame
-PORT = 13676
factor = options.factor
print 'Factor:', factor
+try:
+ rows, columns = map(int, os.popen('stty size', 'r').read().split())
+except Exception:
+ import traceback
+ traceback.print_exc()
+ rows, columns = 25, 80
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
if options.bind_addr:
@@ -133,51 +159,67 @@ if options.bind_addr:
port = '12074'
s.bind((addr, int(port)))
-clients = []
+clients = set()
+targets = set()
uid_groups = {}
type_groups = {}
-
-s.sendto(str(Packet(CMD.PING)), ('255.255.255.255', PORT))
-s.settimeout(options.wait_time)
-
-try:
- while True:
- data, src = s.recvfrom(4096)
- clients.append(src)
-except socket.timeout:
- pass
-
-playing_notes = {}
-for cli in clients:
- playing_notes[cli] = (0, 0)
+ports = {}
+
+if not options.dry:
+ s.settimeout(options.wait_time)
+ for PORT in options.ports:
+ for num in xrange(options.tries):
+ s.sendto(str(Packet(CMD.PING)), ('255.255.255.255', PORT))
+ try:
+ while True:
+ data, src = s.recvfrom(4096)
+ clients.add(src)
+ except socket.timeout:
+ pass
print len(clients), 'detected clients'
-print 'Clients:'
-for cl in clients:
+for num in xrange(options.tries):
+ print 'Try', num
+ for cl in clients:
print cl,
s.sendto(str(Packet(CMD.CAPS)), cl)
data, _ = s.recvfrom(4096)
pkt = Packet.FromStr(data)
print 'ports', pkt.data[0],
+ ports[cl] = pkt.data[0]
tp = itos(pkt.data[1])
print 'type', tp,
uid = ''.join([itos(i) for i in pkt.data[2:]]).rstrip('\x00')
print 'uid', uid
if uid == '':
uid = None
- uid_groups.setdefault(uid, []).append(cl)
- type_groups.setdefault(tp, []).append(cl)
+ uid_groups.setdefault(uid, set()).add(cl)
+ type_groups.setdefault(tp, set()).add(cl)
if options.test:
- ts, tms = int(options.test_delay), int(options.test_delay * 1000000) % 1000000
- s.sendto(str(Packet(CMD.PLAY, ts, tms, 440, options.volume)), cl)
+ ts, tms = int(options.duration), int(options.duration * 1000000) % 1000000
+ if options.wait_test:
+ s.sendto(str(Packet(CMD.PLAY, 65535, 0, 440, options.volume)), cl)
+ raw_input('%r: Press enter to test next client...' %(cl,))
+ s.sendto(str(Packet(CMD.PLAY, ts, tms, 880, options.volume)), cl)
+ else:
+ s.sendto(str(Packet(CMD.PLAY, ts, tms, 440, options.volume)), cl)
if not options.sync_test:
- time.sleep(options.test_delay)
+ time.sleep(options.duration)
s.sendto(str(Packet(CMD.PLAY, ts, tms, 880, options.volume)), cl)
if options.quit:
s.sendto(str(Packet(CMD.QUIT)), cl)
if options.silence:
- s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0)), cl)
+ for i in xrange(pkt.data[0]):
+ s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0.0, i)), cl)
+ if pkt.data[0] == OBLIGATE_POLYPHONE:
+ pkt.data[0] = 1
+ for i in xrange(pkt.data[0]):
+ targets.add(cl+(i,))
+
+playing_notes = {}
+for tg in targets:
+ playing_notes[tg] = (0, 0)
if options.gui:
gui_thr = threading.Thread(target=GUIS[options.gui], args=())
@@ -190,16 +232,16 @@ if options.play:
options.play[i] = int(val[1:])
else:
options.play[i] = int(440.0 * 2**((int(val) - 69)/12.0))
- for i, cl in enumerate(clients):
- s.sendto(str(Packet(CMD.PLAY, int(options.duration), int(1000000*(options.duration-int(options.duration))), options.play[i%len(options.play)], options.volume)), cl)
+ for i, cl in enumerate(targets):
+ s.sendto(str(Packet(CMD.PLAY, int(options.duration), int(1000000*(options.duration-int(options.duration))), options.play[i%len(options.play)], options.volume, cl[2])), cl[:2])
if not options.play_async:
time.sleep(options.duration)
exit()
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)
+ for cl in targets:
+ s.sendto(str(Packet(CMD.PLAY, 0, 250000, 880, options.volume, cl[2])), cl[:2])
if options.test or options.quit or options.silence:
print uid_groups
@@ -208,8 +250,8 @@ if options.test or options.quit or options.silence:
if options.random > 0:
while True:
- for cl in clients:
- s.sendto(str(Packet(CMD.PLAY, int(options.random), int(1000000*(options.random-int(options.random))), random.randint(options.rand_low, options.rand_high), options.volume)), cl)
+ for cl in targets:
+ s.sendto(str(Packet(CMD.PLAY, int(options.random), int(1000000*(options.random-int(options.random))), random.randint(options.rand_low, options.rand_high), options.volume, cl[2])), cl[:2])
time.sleep(options.random)
if options.live or options.list_live:
@@ -223,7 +265,7 @@ if options.live or options.list_live:
print sequencer.SequencerHardware()
exit()
seq = sequencer.SequencerRead(sequencer_resolution=120)
- client_set = set(clients)
+ client_set = set(targets)
active_set = {} # note (pitch) -> [client]
deferred_set = set() # pitches held due to sustain
sustain_status = False
@@ -270,9 +312,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[2])), cli[:2])
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):
@@ -283,7 +325,7 @@ if options.live or options.list_live:
deferred_set.add(event.pitch)
continue
cli = active_set[event.pitch].pop()
- s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0)), cli)
+ s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0, cli[2])), cli[:2])
playing_notes[cli] = (0, 0)
if options.verbose:
print 'LIVE:', event.pitch, '- =>', active_set[event.pitch]
@@ -300,7 +342,7 @@ if options.live or options.list_live:
print 'WARNING: Attempted deferred removal of inactive note %r'%(pitch,)
continue
for cli in active_set[pitch]:
- s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0)), cli)
+ s.sendto(str(Packet(CMD.PLAY, 0, 1, 1, 0, cli[2])), cli[:2])
playing_notes[cli] = (0, 0)
del active_set[pitch]
deferred_set.clear()
@@ -309,6 +351,53 @@ if options.repeat:
args = itertools.cycle(args)
for fname in args:
+ if options.pcm and not fname.endswith('.iv'):
+ print 'PCM: play', fname
+ if fname == '-':
+ import wave
+ pcr = wave.open(sys.stdin)
+ samprate = pcr.getframerate()
+ pcr.read = pcr.readframes
+ else:
+ try:
+ import audiotools
+ pcr = audiotools.open(fname).to_pcm()
+ assert pcr.channels == 1 and pcr.bits_per_sample == 16 and pcr.sample_rate == 44100
+ samprate = pcr.sample_rate
+ except ImportError:
+ import wave
+ pcr = wave.open(fname, 'r')
+ assert pcr.getnchannels() == 1 and pcr.getsampwidth() == 2 and pcr.getframerate() == 44100
+ samprate = pcr.getframerate()
+ pcr.read = pcr.readframes
+
+ def read_all(fn, n):
+ buf = ''
+ while len(buf) < n:
+ nbuf = fn.read(n - len(buf))
+ if not isinstance(nbuf, str):
+ nbuf = nbuf.to_bytes(False, True)
+ buf += nbuf
+ return buf
+
+ BASETIME = time.time() - options.pcmlead
+ sampcnt = 0
+ buf = read_all(pcr, 32)
+ print 'PCM: pcr', pcr, 'BASETIME', BASETIME, 'buf', len(buf)
+ while len(buf) >= 32:
+ frag = buf[:32]
+ buf = buf[32:]
+ for cl in clients:
+ s.sendto(struct.pack('>L', CMD.PCM) + frag, cl)
+ sampcnt += len(frag) / 2
+ delay = max(0, BASETIME + (sampcnt / float(samprate)) - time.time())
+ #print sampcnt, delay
+ if delay > 0:
+ time.sleep(delay)
+ if len(buf) < 32:
+ buf += read_all(pcr, 32 - len(buf))
+ print 'PCM: exit'
+ continue
try:
iv = ET.parse(fname).getroot()
except IOError:
@@ -322,11 +411,12 @@ for fname in args:
number = (len(notestreams) * abs(options.number) if options.number < 0 else options.number)
print len(notestreams), 'notestreams'
print len(clients), 'clients'
+ print len(targets), 'targets'
print len(groups), 'groups'
print number, 'clients used (number)'
class Route(object):
- def __init__(self, fattr, fvalue, group, excl=False):
+ def __init__(self, fattr, fvalue, group, excl=False, complete=False):
if fattr == 'U':
self.map = uid_groups
elif fattr == 'T':
@@ -336,10 +426,9 @@ for fname in args:
else:
raise ValueError('Not a valid attribute specifier: %r'%(fattr,))
self.value = fvalue
- if group is not None and group not in groups:
- raise ValueError('Not a present group: %r'%(group,))
self.group = group
self.excl = excl
+ self.complete = complete
@classmethod
def Parse(cls, s):
fspecs, _, grpspecs = map(lambda x: x.strip(), s.partition('='))
@@ -354,39 +443,51 @@ for fname in args:
ret.append(Route(fattr, fvalue, part[1:], False))
elif part[0] == '-':
ret.append(Route(fattr, fvalue, part[1:], True))
+ elif part[0] == '!':
+ ret.append(Route(fattr, fvalue, part[1:], True, True))
elif part[0] == '0':
ret.append(Route(fattr, fvalue, None, True))
else:
raise ValueError('Not an exclusivity: %r'%(part[0],))
return ret
def Apply(self, cli):
- return cli in self.map.get(self.value, [])
+ return cli[:2] in self.map.get(self.value, [])
def __repr__(self):
return '<Route of %r to %s:%s>'%(self.group, ('U' if self.map is uid_groups else 'T'), self.value)
class RouteSet(object):
def __init__(self, clis=None):
if clis is None:
- clis = clients[:]
- self.clients = clis
+ clis = set(targets)
+ self.clients = list(clis)
self.routes = []
def Route(self, stream):
- testset = self.clients[:]
+ testset = self.clients
grp = stream.get('group', 'ALL')
if options.verbose:
print 'Routing', grp, '...'
excl = False
for route in self.routes:
- if route.group == grp:
+ if route.group is not None and re.match(route.group, grp) is not None:
if options.verbose:
print '\tMatches route', route
excl = excl or route.excl
matches = filter(lambda x, route=route: route.Apply(x), testset)
if matches:
+ if route.complete:
+ if options.verbose:
+ print '\tUsing ALL clients:', matches
+ for cl in matches:
+ self.clients.remove(matches[0])
+ if ports.get(matches[0][:2]) == OBLIGATE_POLYPHONE:
+ self.clients.append(matches[0])
+ return matches
if options.verbose:
print '\tUsing client', matches[0]
self.clients.remove(matches[0])
- return matches[0]
+ if ports.get(matches[0][:2]) == OBLIGATE_POLYPHONE:
+ self.clients.append(matches[0])
+ return [matches[0]]
if options.verbose:
print '\tNo matches, moving on...'
if route.group is None:
@@ -403,16 +504,18 @@ for fname in args:
if excl:
if options.verbose:
print '\tExclusively routed, no route matched.'
- return None
+ return []
if not testset:
if options.verbose:
print '\tOut of clients, no route matched.'
- return None
- cli = testset[0]
+ return []
+ cli = list(testset)[0]
self.clients.remove(cli)
+ if ports.get(cli[:2]) == OBLIGATE_POLYPHONE:
+ self.clients.append(cli)
if options.verbose:
print '\tDefault route to', cli
- return cli
+ return [cli]
routeset = RouteSet()
for rspec in options.routes:
@@ -428,33 +531,50 @@ for fname in args:
print route
class NSThread(threading.Thread):
- def drop_missed(self):
- nsq, cl = self._Thread__args
- cnt = 0
- while nsq and float(nsq[0].get('time'))*factor < time.time() - BASETIME:
- nsq.pop(0)
- cnt += 1
- if options.verbose:
- print self, 'dropped', cnt, 'notes due to miss'
- self._Thread__args = (nsq, cl)
- def wait_for(self, t):
- if t <= 0:
- return
- time.sleep(t)
- def run(self):
- nsq, cl = self._Thread__args
- for note in nsq:
- ttime = float(note.get('time'))
- pitch = int(note.get('pitch')) + options.transpose
- vel = int(note.get('vel'))
- dur = factor*float(note.get('dur'))
- while time.time() - BASETIME < factor*ttime:
- 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)), int(vel*2 * options.volume/255.0))), cl)
- if options.verbose:
- print (time.time() - BASETIME), cl, ': PLAY', pitch, dur, vel
- self.wait_for(dur - ((time.time() - BASETIME) - factor*ttime))
- class NSThread(threading.Thread):
+ def __init__(self, *args, **kwargs):
+ threading.Thread.__init__(self, *args, **kwargs)
+ self.done = False
+ self.cur_offt = None
+ self.next_t = None
+ def actuate_missed(self):
+ nsq, cls = self._Thread__args
+ dur = None
+ i = 0
+ while nsq and float(nsq[0].get('time'))*factor <= time.time() - BASETIME:
+ i += 1
+ note = nsq.pop(0)
+ ttime = float(note.get('time'))
+ 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'))
+ if options.verbose:
+ print (time.time() - BASETIME) / options.factor, ': PLAY', pitch, dur, ampl
+ if options.dry:
+ playing_notes[self.nsid] = (pitch, ampl)
+ else:
+ for cl in cls:
+ s.sendto(str(Packet(CMD.PLAY, int(dur), int((dur*1000000)%1000000), int(440.0 * 2**((pitch-69)/12.0)), ampl * options.volume, cl[2])), cl[:2])
+ playing_notes[cl] = (pitch, ampl)
+ if i > 0 and dur is not None:
+ self.cur_offt = ttime + dur / options.factor
+ else:
+ if self.cur_offt:
+ if factor * self.cur_offt <= time.time() - BASETIME:
+ if options.verbose:
+ print '% 6.5f'%((time.time() - BASETIME) / factor,), ': DONE'
+ self.cur_offt = None
+ if options.dry:
+ playing_notes[self.nsid] = (0, 0)
+ else:
+ for cl in cls:
+ playing_notes[cl] = (0, 0)
+ next_act = None
+ if nsq:
+ next_act = float(nsq[0].get('time'))
+ if options.verbose:
+ print 'NEXT_ACT:', next_act, 'CUR_OFFT:', self.cur_offt
+ self.next_t = min((next_act or float('inf'), self.cur_offt or float('inf')))
+ self.done = not (nsq or self.cur_offt)
def drop_missed(self):
nsq, cl = self._Thread__args
cnt = 0
@@ -463,7 +583,6 @@ for fname in args:
cnt += 1
if options.verbose:
print self, 'dropped', cnt, 'notes due to miss'
- self._Thread__args = (nsq, cl)
def wait_for(self, t):
if t <= 0:
return
@@ -473,30 +592,42 @@ 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', float(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)
+ self.wait_for(factor*ttime - (time.time() - BASETIME))
+ if options.dry:
+ cl = self.nsid # XXX hack
+ else:
+ for cl in cls:
+ s.sendto(str(Packet(CMD.PLAY, int(dur), int((dur*1000000)%1000000), int(440.0 * 2**((pitch-69)/12.0)), ampl * options.volume, cl[2])), cl[:2])
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:
print '% 6.5f'%(time.time() - BASETIME,), cl, ': DONE'
threads = {}
- nscycle = itertools.cycle(notestreams)
- for idx, ns in zip(xrange(number), nscycle):
- cli = routeset.Route(ns)
- if cli:
+ if options.dry:
+ for nsid, ns in enumerate(notestreams):
nsq = ns.findall('note')
- if ns in threads:
- threads[ns]._Thread__args[1].add(cli)
- else:
- threads[ns] = NSThread(args=(nsq, set([cli])))
+ nsq.sort(key=lambda x: float(x.get('time')))
+ threads[ns] = NSThread(args=(nsq, set()))
+ threads[ns].nsid = nsid
+ targets = threads.values() # XXX hack
+ else:
+ nscycle = itertools.cycle(notestreams)
+ for idx, ns in zip(xrange(number), nscycle):
+ clis = routeset.Route(ns)
+ for cli in clis:
+ nsq = ns.findall('note')
+ nsq.sort(key=lambda x: float(x.get('time')))
+ if ns in threads:
+ threads[ns]._Thread__args[1].add(cli)
+ else:
+ threads[ns] = NSThread(args=(nsq, set([cli])))
if options.verbose:
print 'Playback threads:'
@@ -504,11 +635,31 @@ for fname in args:
print thr._Thread__args[1]
BASETIME = time.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())
+ print 'Playtime is', ENDTIME
if options.seek > 0:
for thr in threads.values():
thr.drop_missed()
- for thr in threads.values():
- thr.start()
- for thr in threads.values():
- thr.join()
+ spin_phase = 0
+ SPINNERS = ['-', '\\', '|', '/']
+ while not all(thr.done for thr in threads.values()):
+ for thr in threads.values():
+ if thr.next_t is None or factor * thr.next_t <= time.time() - BASETIME:
+ thr.actuate_missed()
+ delta = factor * min(thr.next_t for thr in threads.values() if thr.next_t is not None) + BASETIME - time.time()
+ if delta == float('inf'):
+ print 'WARNING: Infinite postponement detected! Did all notestreams finish?'
+ break
+ if options.verbose:
+ print 'TICK DELTA:', delta
+ else:
+ sys.stdout.write('\x1b[G\x1b[K[%s]' % (
+ ('#' * int((time.time() - BASETIME) * (columns - 2) / (ENDTIME * factor)) + SPINNERS[spin_phase]).ljust(columns - 2),
+ ))
+ sys.stdout.flush()
+ spin_phase += 1
+ if spin_phase >= len(SPINNERS):
+ spin_phase = 0
+ if delta >= 0 and not options.spin:
+ time.sleep(delta)
print fname, ': Done!'
diff --git a/client.py b/client.py
index 2d1ab40..2fcaae9 100644
--- a/client.py
+++ b/client.py
@@ -13,6 +13,7 @@ import array
import random
import threading
import thread
+import colorsys
from packet import Packet, CMD, stoi
@@ -24,34 +25,58 @@ parser.add_option('-u', '--uid', dest='uid', default='', help='Set the UID (iden
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')
parser.add_option('-V', '--volume', dest='volume', type='float', default=1.0, help='Set the volume factor (>1 distorts, <1 attenuates)')
+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('--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)')
parser.add_option('--pg-height', dest='height', type='int', help='Set the height of the window or full-screen video mode')
+parser.add_option('--pg-no-colback', dest='no_colback', action='store_true', help='Don\'t render a colored background')
+parser.add_option('--pg-low-freq', dest='low_freq', type='int', default=40, help='Low frequency for colored background')
+parser.add_option('--pg-high-freq', dest='high_freq', type='int', default=1500, help='High frequency for colored background')
+parser.add_option('--pg-log-base', dest='log_base', type='int', default=2, help='Logarithmic base for coloring (0 to make linear)')
+parser.add_option('--counter-modulus', dest='counter_modulus', type='int', default=16, help='Number of packet events in period of the terminal color scroll on the left margin')
options, args = parser.parse_args()
+if options.numpy:
+ import numpy
+
PORT = options.port
-STREAMS = 1
+STREAMS = options.streams
IDENT = 'TONE'
UID = options.uid
-LAST_SAMP = 0
+LAST_SAMPS = [0] * STREAMS
LAST_SAMPLES = []
-FREQ = 0
-PHASE = 0
+FREQS = [0] * STREAMS
+PHASES = [0] * STREAMS
RATE = options.rate
FPB = 64
Z_SAMP = '\x00\x00\x00\x00'
MAX = 0x7fffffff
-AMP = MAX
+AMPS = [MAX] * STREAMS
MIN = -0x80000000
+EXPIRATIONS = [0] * STREAMS
+QUEUED_PCM = ''
+
def lin_interp(frm, to, p):
return p*to + (1-p)*frm
+def rgb_for_freq_amp(f, a):
+ a = max((min((a, 1.0)), 0.0))
+ pitchval = float(f - options.low_freq) / (options.high_freq - options.low_freq)
+ if options.log_base == 0:
+ try:
+ pitchval = math.log(pitchval) / math.log(options.log_base)
+ except ValueError:
+ pass
+ bgcol = colorsys.hls_to_rgb(min((1.0, max((0.0, pitchval)))), 0.5 * (a ** 2), 1.0)
+ return [int(i*255) for i in bgcol]
+
# GUIs
GUIs = {}
@@ -95,23 +120,42 @@ def pygame_notes():
PFAC = HEIGHT / 128.0
sampwin = pygame.Surface((SAMP_WIDTH, HEIGHT))
+ sampwin.set_colorkey((0, 0, 0))
lastsy = HEIGHT / 2
+ bgrwin = pygame.Surface((BGR_WIDTH, HEIGHT))
+ bgrwin.set_colorkey((0, 0, 0))
clock = pygame.time.Clock()
while True:
- if FREQ > 0:
- try:
- pitch = 12 * math.log(FREQ / 440.0, 2) + 69
- except ValueError:
- pitch = 0
+ if options.no_colback:
+ disp.fill((0, 0, 0), (0, 0, WIDTH, HEIGHT))
else:
- pitch = 0
- col = [int((AMP / MAX) * 255)] * 3
-
- disp.fill((0, 0, 0), (BGR_WIDTH, 0, SAMP_WIDTH, HEIGHT))
- disp.scroll(-1, 0)
- disp.fill(col, (BGR_WIDTH - 1, HEIGHT - pitch * PFAC - PFAC, 1, PFAC))
+ gap = WIDTH / STREAMS
+ for i in xrange(STREAMS):
+ FREQ = FREQS[i]
+ AMP = AMPS[i]
+ if FREQ > 0:
+ bgcol = rgb_for_freq_amp(FREQ, float(AMP) / MAX)
+ else:
+ bgcol = (0, 0, 0)
+ #print i, ':', pitchval
+ disp.fill(bgcol, (i*gap, 0, gap, HEIGHT))
+
+ bgrwin.scroll(-1, 0)
+ bgrwin.fill((0, 0, 0), (BGR_WIDTH - 1, 0, 1, HEIGHT))
+ for i in xrange(STREAMS):
+ FREQ = FREQS[i]
+ AMP = AMPS[i]
+ if FREQ > 0:
+ try:
+ pitch = 12 * math.log(FREQ / 440.0, 2) + 69
+ except ValueError:
+ pitch = 0
+ else:
+ pitch = 0
+ col = [int((AMP / MAX) * 255)] * 3
+ bgrwin.fill(col, (BGR_WIDTH - 1, HEIGHT - pitch * PFAC - PFAC, 1, PFAC))
sampwin.scroll(-len(LAST_SAMPLES), 0)
x = max(0, SAMP_WIDTH - len(LAST_SAMPLES))
@@ -132,6 +176,7 @@ def pygame_notes():
# break
#if len(pts) > 2:
# pygame.gfxdraw.aapolygon(disp, pts, [0, 255, 0])
+ disp.blit(bgrwin, (0, 0))
disp.blit(sampwin, (BGR_WIDTH, 0))
pygame.display.flip()
@@ -272,45 +317,85 @@ if options.generators:
#generator = square_wave
generator = eval(options.generator)
-def sigalrm(sig, frm):
- global FREQ
- FREQ = 0
-
-def lin_seq(frm, to, cnt):
- step = (to-frm)/float(cnt)
- samps = [0]*cnt
- for i in xrange(cnt):
- p = i / float(cnt-1)
- samps[i] = int(lin_interp(frm, to, p))
- return samps
-
-def samps(freq, phase, cnt):
- global RATE, AMP
- samps = [0]*cnt
- for i in xrange(cnt):
- samps[i] = int(AMP * max(-1, min(1, options.volume*generator((phase + 2 * math.pi * freq * i / RATE) % (2*math.pi)))))
- return samps, (phase + 2 * math.pi * freq * cnt / RATE) % (2*math.pi)
-
-def to_data(samps):
- return struct.pack('i'*len(samps), *samps)
-
-def gen_data(data, frames, time, status):
- global FREQ, PHASE, Z_SAMP, LAST_SAMP, LAST_SAMPLES
- if FREQ == 0:
- PHASE = 0
- if LAST_SAMP == 0:
- if options.gui:
- LAST_SAMPLES.extend([0]*frames)
- return (Z_SAMP*frames, pyaudio.paContinue)
- fdata = lin_seq(LAST_SAMP, 0, frames)
- if options.gui:
- LAST_SAMPLES.extend(fdata)
- LAST_SAMP = fdata[-1]
- return (to_data(fdata), pyaudio.paContinue)
- fdata, PHASE = samps(FREQ, PHASE, frames)
+#def sigalrm(sig, frm):
+# global FREQ
+# FREQ = 0
+
+if options.numpy:
+ def lin_seq(frm, to, cnt):
+ return numpy.linspace(frm, to, cnt, dtype=numpy.int32)
+
+ def samps(freq, amp, phase, cnt):
+ samps = numpy.ndarray((cnt,), numpy.int32)
+ pvel = 2 * math.pi * freq / RATE
+ fac = options.volume * amp / float(STREAMS)
+ for i in xrange(cnt):
+ samps[i] = fac * max(-1, min(1, generator(phase)))
+ phase = (phase + pvel) % (2 * math.pi)
+ return samps, phase
+
+ def to_data(samps):
+ return samps.tobytes()
+
+ def mix(a, b):
+ return a + b
+
+else:
+ def lin_seq(frm, to, cnt):
+ step = (to-frm)/float(cnt)
+ samps = [0]*cnt
+ for i in xrange(cnt):
+ p = i / float(cnt-1)
+ samps[i] = int(lin_interp(frm, to, p))
+ return samps
+
+ def samps(freq, amp, phase, cnt):
+ global RATE
+ samps = [0]*cnt
+ for i in xrange(cnt):
+ samps[i] = int(2*amp / float(STREAMS) * max(-1, min(1, options.volume*generator((phase + 2 * math.pi * freq * i / RATE) % (2*math.pi)))))
+ return samps, (phase + 2 * math.pi * freq * cnt / RATE) % (2*math.pi)
+
+ def to_data(samps):
+ return struct.pack('i'*len(samps), *samps)
+
+ def mix(a, b):
+ return [min(MAX, max(MIN, i + j)) for i, j in zip(a, b)]
+
+def gen_data(data, frames, tm, status):
+ global FREQS, PHASE, Z_SAMP, LAST_SAMP, LAST_SAMPLES, QUEUED_PCM
+ if len(QUEUED_PCM) >= frames*4:
+ fdata = QUEUED_PCM[:frames*4]
+ QUEUED_PCM = QUEUED_PCM[frames*4:]
+ LAST_SAMPLES.extend(struct.unpack(str(frames)+'i', fdata))
+ return fdata, pyaudio.paContinue
+ if options.numpy:
+ fdata = numpy.zeros((frames,), numpy.int32)
+ else:
+ fdata = [0] * frames
+ for i in range(STREAMS):
+ FREQ = FREQS[i]
+ LAST_SAMP = LAST_SAMPS[i]
+ AMP = AMPS[i]
+ EXPIRATION = EXPIRATIONS[i]
+ PHASE = PHASES[i]
+ if FREQ != 0:
+ if time.time() > EXPIRATION:
+ 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)
+ LAST_SAMPS[i] = vdata[-1]
+ else:
+ vdata, PHASE = samps(FREQ, AMP, PHASE, frames)
+ fdata = mix(fdata, vdata)
+ PHASES[i] = PHASE
+ LAST_SAMPS[i] = vdata[-1]
if options.gui:
LAST_SAMPLES.extend(fdata)
- LAST_SAMP = fdata[-1]
return (to_data(fdata), pyaudio.paContinue)
pa = pyaudio.PyAudio()
@@ -322,21 +407,25 @@ if options.gui:
guithread.start()
if options.test:
- FREQ = 440
+ FREQS[0] = 440
+ EXPIRATIONS[0] = time.time() + 1
time.sleep(1)
- FREQ = 0
+ FREQS[0] = 0
time.sleep(1)
- FREQ = 880
+ FREQS[0] = 880
+ EXPIRATIONS[0] = time.time() + 1
time.sleep(1)
- FREQ = 440
+ FREQS[0] = 440
+ EXPIRATIONS[0] = time.time() + 2
time.sleep(2)
exit()
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', PORT))
-signal.signal(signal.SIGALRM, sigalrm)
+#signal.signal(signal.SIGALRM, sigalrm)
+counter = 0
while True:
data = ''
while not data:
@@ -345,24 +434,46 @@ while True:
except socket.error:
pass
pkt = Packet.FromStr(data)
- print 'From', cli, 'command', pkt.cmd
+ crgb = [int(i*255) for i in colorsys.hls_to_rgb((float(counter) / options.counter_modulus) % 1.0, 0.5, 1.0)]
+ print '\x1b[38;2;{};{};{}m#'.format(*crgb),
+ counter += 1
+ print '\x1b[mFrom', cli, 'command', pkt.cmd,
if pkt.cmd == CMD.KA:
- pass
+ print '\x1b[37mKA'
elif pkt.cmd == CMD.PING:
sock.sendto(data, cli)
+ print '\x1b[1;33mPING'
elif pkt.cmd == CMD.QUIT:
+ print '\x1b[1;31mQUIT'
break
elif pkt.cmd == CMD.PLAY:
+ voice = pkt.data[4]
dur = pkt.data[0]+pkt.data[1]/1000000.0
- FREQ = pkt.data[2]
- AMP = MAX * (pkt.data[3]/255.0)
- signal.setitimer(signal.ITIMER_REAL, dur)
+ FREQS[voice] = pkt.data[2]
+ AMPS[voice] = MAX * max(min(pkt.as_float(3), 1.0), 0.0)
+ EXPIRATIONS[voice] = time.time() + dur
+ 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',
+ print '\x1b[1;38;2;{};{};{}mVOICE'.format(*vrgb), '{:03}'.format(voice),
+ print '\x1b[1;38;2;{};{};{}mFREQ'.format(*frgb), '{:04}'.format(pkt.data[2]), 'AMP', '%08.6f'%pkt.as_float(3),
+ if pkt.data[0] == 0 and pkt.data[1] == 0:
+ print '\x1b[1;35mSTOP!!!'
+ else:
+ print '\x1b[1;36mDUR', '%08.6f'%dur
+ #signal.setitimer(signal.ITIMER_REAL, dur)
elif pkt.cmd == CMD.CAPS:
data = [0] * 8
data[0] = STREAMS
data[1] = stoi(IDENT)
- for i in xrange(len(UID)/4):
+ for i in xrange(len(UID)/4 + 1):
data[i+2] = stoi(UID[4*i:4*(i+1)])
sock.sendto(str(Packet(CMD.CAPS, *data)), cli)
+ print '\x1b[1;34mCAPS'
+ elif pkt.cmd == CMD.PCM:
+ fdata = data[4:]
+ fdata = struct.pack('16i', *[i<<16 for i in struct.unpack('16h', fdata)])
+ QUEUED_PCM += fdata
+ print 'Now', len(QUEUED_PCM) / 4.0, 'frames queued'
else:
print 'Unknown cmd', pkt.cmd
diff --git a/downsamp.py b/downsamp.py
new file mode 100644
index 0000000..f7a0255
--- /dev/null
+++ b/downsamp.py
@@ -0,0 +1,117 @@
+from xml.etree import ElementTree as ET
+import optparse
+import os
+
+parser = optparse.OptionParser()
+parser.add_option('-f', '--frequency', dest='frequency', type='float', help='How often to switch between active streams')
+parser.set_defaults(frequency=0.016)
+options, args = parser.parse_args()
+
+class Note(object):
+ def __init__(self, time, dur, pitch, ampl):
+ self.time = time
+ self.dur = dur
+ self.pitch = pitch
+ self.ampl = ampl
+
+for fname in args:
+ try:
+ iv = ET.parse(fname).getroot()
+ except IOError:
+ import traceback
+ traceback.print_exc()
+ print fname, ': Bad file'
+ continue
+
+ print '----', fname, '----'
+
+ notestreams = iv.findall("./streams/stream[@type='ns']")
+ print len(notestreams), 'notestreams'
+
+ print 'Loading all events...'
+
+ evs = []
+
+ dur = 0.0
+
+ for ns in notestreams:
+ for note in ns.findall('note'):
+ n = Note(
+ float(note.get('time')),
+ float(note.get('dur')),
+ float(note.get('pitch')),
+ float(note.get('ampl', float(note.get('vel', 127.0)) / 127.0)),
+ )
+ evs.append(n)
+ if n.time + n.dur > dur:
+ dur = n.time + n.dur
+
+ print len(evs), 'events'
+ print dur, 'duration'
+
+ print 'Scheduling events...'
+
+ sched = {}
+
+ t = 0.0
+ i = 0
+ while t <= dur:
+ nextt = t + options.frequency
+ #print '-t', t, 'nextt', nextt
+
+ evs_now = [n for n in evs if n.time <= t and t < n.time + n.dur]
+ if evs_now:
+ holding = False
+ count = 0
+ while count < len(evs_now):
+ selidx = (count + i) % len(evs_now)
+ sel = evs_now[selidx]
+ sched[t] = (sel.pitch, sel.ampl)
+ if sel.time + sel.dur >= nextt:
+ holding = True
+ break
+ t = sel.time + sel.dur
+ count += 1
+ if not holding:
+ sched[t] = (0, 0)
+ else:
+ sched[t] = (0, 0)
+
+ t = nextt
+ i += 1
+
+ print len(sched), 'events scheduled'
+
+ print 'Writing out schedule...'
+
+ newiv = ET.Element('iv')
+ newiv.append(iv.find('meta'))
+ newivstreams = ET.SubElement(newiv, 'streams')
+ newivstream = ET.SubElement(newivstreams, 'stream', type='ns')
+
+ prevt = None
+ prevev = None
+ for t, ev in sorted(sched.items(), key=lambda pair: pair[0]):
+ if prevt is not None:
+ if prevev[0] != 0:
+ ET.SubElement(newivstream, 'note',
+ pitch = str(prevev[0]),
+ ampl = str(prevev[1]),
+ time = str(prevt),
+ dur = str(t - prevt),
+ )
+ prevev = ev
+ prevt = t
+
+ t = dur
+ if prevev[0] != 0:
+ ET.SubElement(newivstream, 'note',
+ pitch = str(prevev[0]),
+ ampl = str(prevev[1]),
+ time = str(prevt),
+ dur = str(t - prevt),
+ )
+
+ print 'Done.'
+ txt = ET.tostring(newiv, 'UTF-8')
+ open(os.path.splitext(os.path.basename(fname))[0]+'.downsampled.iv', 'wb').write(txt)
diff --git a/drums.py b/drums.py
new file mode 100644
index 0000000..d3c1a58
--- /dev/null
+++ b/drums.py
@@ -0,0 +1,215 @@
+import pyaudio
+import socket
+import optparse
+import tarfile
+import wave
+import cStringIO as StringIO
+import array
+import time
+import colorsys
+
+from packet import Packet, CMD, stoi, OBLIGATE_POLYPHONE
+
+parser = optparse.OptionParser()
+parser.add_option('-t', '--test', dest='test', action='store_true', help='As a test, play all samples then exit')
+parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Be verbose')
+parser.add_option('-V', '--volume', dest='volume', type='float', default=1.0, help='Set the volume factor (nominally [0.0, 1.0], but >1.0 can be used to amplify with possible distortion)')
+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=13676, type='int', help='UDP port to listen on')
+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)')
+parser.add_option('--pg-low-freq', dest='low_freq', type='int', default=40, help='Low frequency for colored background')
+parser.add_option('--pg-high-freq', dest='high_freq', type='int', default=1500, help='High frequency for colored background')
+parser.add_option('--pg-log-base', dest='log_base', type='int', default=2, help='Logarithmic base for coloring (0 to make linear)')
+parser.add_option('--counter-modulus', dest='counter_modulus', type='int', default=16, help='Number of packet events in period of the terminal color scroll on the left margin')
+
+options, args = parser.parse_args()
+
+MAX = 0x7fffffff
+MIN = -0x80000000
+IDENT = 'DRUM'
+
+if not args:
+ print 'Need at least one drumpack (.tar.bz2) as an argument!'
+ parser.print_usage()
+ exit(1)
+
+def rgb_for_freq_amp(f, a):
+ pitchval = float(f - options.low_freq) / (options.high_freq - options.low_freq)
+ a = max((min((a, 1.0)), 0.0))
+ if options.log_base == 0:
+ try:
+ pitchval = math.log(pitchval) / math.log(options.log_base)
+ except ValueError:
+ pass
+ bgcol = colorsys.hls_to_rgb(min((1.0, max((0.0, pitchval)))), 0.5 * (a ** 2), 1.0)
+ return [int(i*255) for i in bgcol]
+
+DRUMS = {}
+
+for fname in args:
+ print 'Reading', fname, '...'
+ tf = tarfile.open(fname, 'r')
+ names = tf.getnames()
+ for nm in names:
+ if not (nm.endswith('.wav') or nm.endswith('.raw')) or len(nm) < 5:
+ continue
+ frq = int(nm[:-4])
+ if options.verbose:
+ print '\tLoading frq', frq, '...'
+ fo = tf.extractfile(nm)
+ if nm.endswith('.wav'):
+ wf = wave.open(fo)
+ if wf.getnchannels() != 1:
+ print '\t\tWARNING: Channel count wrong: got', wf.getnchannels(), 'expecting 1'
+ if wf.getsampwidth() != 4:
+ print '\t\tWARNING: Sample width wrong: got', wf.getsampwidth(), 'expecting 4'
+ if wf.getframerate() != options.rate:
+ print '\t\tWARNING: Rate wrong: got', wf.getframerate(), 'expecting', options.rate, '(maybe try setting -r?)'
+ frames = wf.getnframes()
+ data = ''
+ while len(data) < wf.getsampwidth() * frames:
+ data += wf.readframes(frames - len(data) / wf.getsampwidth())
+ elif nm.endswith('.raw'):
+ data = fo.read()
+ frames = len(data) / 4
+ if options.verbose:
+ print '\t\tData:', frames, 'samples,', len(data), 'bytes'
+ if frq in DRUMS:
+ print '\t\tWARNING: frequency', frq, 'already in map, overwriting...'
+ DRUMS[frq] = data
+
+if options.verbose:
+ print len(DRUMS), 'sounds loaded'
+
+PLAYING = []
+
+class SampleReader(object):
+ def __init__(self, buf, total, amp):
+ self.buf = buf
+ self.total = total
+ self.cur = 0
+ self.amp = amp
+
+ def read(self, bytes):
+ if self.cur >= self.total:
+ return ''
+ res = ''
+ while self.cur < self.total and len(res) < bytes:
+ data = self.buf[self.cur % len(self.buf):self.cur % len(self.buf) + bytes - len(res)]
+ self.cur += len(data)
+ res += data
+ arr = array.array('i')
+ arr.fromstring(res)
+ for i in range(len(arr)):
+ arr[i] = int(arr[i] * self.amp)
+ return arr.tostring()
+
+ def __repr__(self):
+ return '<SR (%d) @%d / %d A:%f>'%(len(self.buf), self.cur, self.total, self.amp)
+
+def gen_data(data, frames, tm, status):
+ fdata = array.array('l', [0] * frames)
+ torem = set()
+ for src in set(PLAYING):
+ buf = src.read(frames * 4)
+ if not buf:
+ torem.add(src)
+ continue
+ samps = array.array('i')
+ samps.fromstring(buf)
+ if len(samps) < frames:
+ samps.extend([0] * (frames - len(samps)))
+ for i in range(frames):
+ fdata[i] += samps[i]
+ for src in torem:
+ PLAYING.remove(src)
+ for i in range(frames):
+ fdata[i] = max(MIN, min(MAX, fdata[i]))
+ fdata = array.array('i', fdata)
+ return (fdata.tostring(), pyaudio.paContinue)
+
+pa = pyaudio.PyAudio()
+stream = pa.open(rate=options.rate, channels=1, format=pyaudio.paInt32, output=True, frames_per_buffer=64, stream_callback=gen_data)
+
+if options.test:
+ for frq in sorted(DRUMS.keys()):
+ print 'Current playing:', PLAYING
+ print 'Playing:', frq
+ data = DRUMS[frq]
+ PLAYING.append(SampleReader(data, len(data), options.volume))
+ time.sleep(len(data) / (4.0 * options.rate))
+ print 'Done'
+ exit()
+
+
+sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+sock.bind(('', options.port))
+
+#signal.signal(signal.SIGALRM, sigalrm)
+
+counter = 0
+while True:
+ data = ''
+ while not data:
+ try:
+ data, cli = sock.recvfrom(4096)
+ except socket.error:
+ pass
+ pkt = Packet.FromStr(data)
+ crgb = [int(i*255) for i in colorsys.hls_to_rgb((float(counter) / options.counter_modulus) % 1.0, 0.5, 1.0)]
+ print '\x1b[38;2;{};{};{}m#'.format(*crgb),
+ counter += 1
+ print '\x1b[mFrom', cli, 'command', pkt.cmd,
+ if pkt.cmd == CMD.KA:
+ print '\x1b[37mKA'
+ elif pkt.cmd == CMD.PING:
+ sock.sendto(data, cli)
+ print '\x1b[1;33mPING'
+ elif pkt.cmd == CMD.QUIT:
+ print '\x1b[1;31mQUIT'
+ break
+ elif pkt.cmd == CMD.PLAY:
+ frq = pkt.data[2]
+ if frq not in DRUMS:
+ print 'WARNING: No such instrument', frq, ', ignoring...'
+ continue
+ rdata = DRUMS[frq]
+ rframes = len(rdata) / 4
+ dur = pkt.data[0]+pkt.data[1]/1000000.0
+ dframes = int(dur * options.rate)
+ if not options.repeat:
+ 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)
+ PLAYING.append(SampleReader(rdata, dframes * 4, amp))
+ if options.max_voices >= 0:
+ while len(PLAYING) > options.max_voices:
+ PLAYING.pop(0)
+ frgb = rgb_for_freq_amp(pkt.data[2], pkt.as_float(3))
+ print '\x1b[1;32mPLAY',
+ print '\x1b[1;34mVOICE', '{:03}'.format(pkt.data[4]),
+ print '\x1b[1;38;2;{};{};{}mFREQ'.format(*frgb), '{:04}'.format(pkt.data[2]), 'AMP', '%08.6f'%pkt.as_float(3),
+ if pkt.data[0] == 0 and pkt.data[1] == 0:
+ print '\x1b[1;35mSTOP!!!'
+ else:
+ print '\x1b[1;36mDUR', '%08.6f'%dur
+ #signal.setitimer(signal.ITIMER_REAL, dur)
+ elif pkt.cmd == CMD.CAPS:
+ data = [0] * 8
+ data[0] = OBLIGATE_POLYPHONE
+ data[1] = stoi(IDENT)
+ for i in xrange(len(options.uid)/4 + 1):
+ data[i+2] = stoi(options.uid[4*i:4*(i+1)])
+ sock.sendto(str(Packet(CMD.CAPS, *data)), cli)
+ print '\x1b[1;34mCAPS'
+# elif pkt.cmd == CMD.PCM:
+# fdata = data[4:]
+# fdata = struct.pack('16i', *[i<<16 for i in struct.unpack('16h', fdata)])
+# QUEUED_PCM += fdata
+# print 'Now', len(QUEUED_PCM) / 4.0, 'frames queued'
+ else:
+ print 'Unknown cmd', pkt.cmd
diff --git a/drums.tar.bz2 b/drums.tar.bz2
new file mode 100644
index 0000000..d769e1b
--- /dev/null
+++ b/drums.tar.bz2
Binary files differ
diff --git a/make_patfile.sh b/make_patfile.sh
new file mode 100755
index 0000000..b656d2d
--- /dev/null
+++ b/make_patfile.sh
@@ -0,0 +1,29 @@
+# Convert the FreePats Drums_000 directory to ITL Chorus drums.tar.bz2
+# Note: technically, this must be run in a directory with subdirectories
+# starting with a MIDI pitch, and containing a text file with a
+# "convert_to_wav:" command that produces a .wav in that working directory.
+# sox is required to convert the audio. This handles the dirt-old options
+# for sox in the text files explicitly to support the FreePats standard.
+# The current version was checked in from the Drums_000 directory to be
+# found in the TAR at this URL:
+# http://freepats.zenvoid.org/samples/freepats/freepats-raw-samples.tar.bz2
+# Thank you again, FreePats!
+
+rm *.wav .wav *.raw .raw
+
+for i in *; do
+ if [ -d $i ]; then
+ pushd $i
+ eval `grep 'convert_to_wav' *.txt | sed -e 's/convert_to_wav: //' -e 's/-w/-b 16/' -e 's/-s/-e signed/' -e 's/-u/-e unsigned/'`
+ PITCH=`echo "$i" | sed -e 's/^\([0-9]\+\).*$/\1/g'`
+ # From broadcast.py, eval'd in Python for consistent results
+ FRQ=`echo $PITCH | python2 -c "print(int(440.0 * 2**((int(raw_input())-69)/12.0)))"`
+ echo "WRITING $FRQ.wav"
+ [ -z "$FRQ" ] && echo "!!! EMPTY FILENAME?"
+ sox *.wav -r 44100 -c 1 -e signed -b 32 -t raw ../$FRQ.raw
+ popd
+ fi
+done
+
+rm drums.tar.bz2
+tar cjf drums.tar.bz2 *.raw
diff --git a/mkiv.py b/mkiv.py
index 717220c..0c87372 100644
--- a/mkiv.py
+++ b/mkiv.py
@@ -4,10 +4,6 @@ mkiv -- Make Intervals
This simple script (using python-midi) reads a MIDI file and makes an interval
(.iv) file (actually XML) that contains non-overlapping notes.
-
-TODO:
--MIDI Control events
--Percussion
'''
import xml.etree.ElementTree as ET
@@ -15,6 +11,7 @@ import midi
import sys
import os
import optparse
+import math
TRACKS = object()
PROGRAMS = object()
@@ -32,8 +29,24 @@ 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('--modwheel-continuous', dest='modcont', action='store_true', help='Keep phase continuous in global time (don\'t reset to 0 for each note)')
+parser.add_option('--string-res', dest='stringres', type='float', help='(Fractional) seconds by which to resolve string models (0 to disable)')
+parser.add_option('--string-max', dest='stringmax', type='int', help='Maximum number of events to generate per single input event')
+parser.add_option('--string-rate-on', dest='stringonrate', type='float', help='Rate (amplitude / sec) by which to exponentially decay in the string model while a note is active')
+parser.add_option('--string-rate-off', dest='stringoffrate', type='float', help='Rate (amplitude / sec) by which to exponentially decay in the string model after a note ends')
+parser.add_option('--string-threshold', dest='stringthres', type='float', help='Amplitude (as fraction of original) at which point the string model event is terminated')
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('--epsilon', dest='epsilon', type='float', help='Don\'t consider overlaps smaller than this number of seconds (which regularly happen due to precision loss)')
+parser.add_option('--slack', dest='slack', type='float', help='Inflate the duration of events by this much when scheduling them--this is for clients which need time to release their streams')
+parser.add_option('--vol-pow', dest='vol_pow', type='float', help='Exponent to raise volume changes (adjusts energy per delta volume)')
+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.add_option('--no-text', dest='no_text', action='store_true', help='Disable text streams (useful for unusual text encodings)')
+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, vol_pow=2)
options, args = parser.parse_args()
if options.tempo == 'f1':
options.tempo == 'global'
@@ -49,6 +62,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,25 +228,31 @@ 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)
+
+ vol_at = [[{0: 0x3FFF} for i in range(16)] for j in range(len(pat))]
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))]
+ chg_vol = [[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))]
tnames = [''] * len(pat)
progs = set([0])
@@ -253,25 +273,40 @@ for fname in args:
progs.add(ev.value)
chg_prog[tidx][ev.channel] += 1
elif isinstance(ev, midi.ControlChangeEvent):
- if ev.control == 0:
- cur_bank[tidx][ev.channel] = (0x3F80 & cur_bank[tidx][ev.channel]) | ev.value
- chg_bank[tidx][ev.channel] += 1
- elif ev.control == 32:
+ if ev.control == 0: # Bank -- MSB
cur_bank[tidx][ev.channel] = (0x3F & cur_bank[tidx][ev.channel]) | (ev.value << 7)
chg_bank[tidx][ev.channel] += 1
+ elif ev.control == 32: # Bank -- LSB
+ cur_bank[tidx][ev.channel] = (0x3F80 & cur_bank[tidx][ev.channel]) | ev.value
+ chg_bank[tidx][ev.channel] += 1
+ elif ev.control == 1: # ModWheel -- MSB
+ cur_mw[tidx][ev.channel] = (0x3F & cur_mw[tidx][ev.channel]) | (ev.value << 7)
+ chg_mw[tidx][ev.channel] += 1
+ elif ev.control == 33: # ModWheel -- LSB
+ cur_mw[tidx][ev.channel] = (0x3F80 & cur_mw[tidx][ev.channel]) | ev.value
+ chg_mw[tidx][ev.channel] += 1
+ elif ev.control == 7: # Volume -- MSB
+ lvtime, lvol = sorted(vol_at[tidx][ev.channel].items(), key = lambda pair: pair[0])[-1]
+ vol_at[tidx][ev.channel][abstime] = (0x3F & lvol) | (ev.value << 7)
+ chg_vol[tidx][ev.channel] += 1
+ elif ev.control == 39: # Volume -- LSB
+ lvtime, lvol = sorted(vol_at[tidx][ev.channel].items(), key = lambda pair: pair[0])[-1]
+ vol_at[tidx][ev.channel][abstime] = (0x3F80 & lvol) | ev.value
+ chg_vol[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:
- print 'Track name, event count, final banks, bank changes, final programs, program changes:'
- for tidx, tname in enumerate(tnames):
- print tidx, ':', tname, ',', ','.join(map(str, ev_cnts[tidx])), ',', ','.join(map(str, cur_bank[tidx])), ',', ','.join(map(str, chg_bank[tidx])), ',', ','.join(map(str, cur_prog[tidx])), ',', ','.join(map(str, chg_prog[tidx]))
- print 'All programs observed:', progs
+ print 'Track name, event count, final banks, bank changes, final programs, program changes, final modwheel, modwheel changes, volume changes:'
+ for tidx, tname in enumerate(tnames):
+ print tidx, ':', tname, ',', ','.join(map(str, ev_cnts[tidx])), ',', ','.join(map(str, cur_bank[tidx])), ',', ','.join(map(str, chg_bank[tidx])), ',', ','.join(map(str, cur_prog[tidx])), ',', ','.join(map(str, chg_prog[tidx])), ',', ','.join(map(str, cur_mw[tidx])), ',', ','.join(map(str, chg_mw[tidx])), ',', ','.join(map(str, chg_vol[tidx]))
+ print 'All programs observed:', progs
print 'Sorting events...'
@@ -281,29 +316,39 @@ 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', 'real_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.real_duration = dur
+ self.modwheel = modwheel
+
+ 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', '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.bentpitch, self.active.ev.velocity / 127.0, mev.abstime - self.active.abstime, self.modwheel))
self.active = None
- self.realpitch = None
+ self.bentpitch = None
+ self.modwheel = 0
def WouldDeactivate(self, mev):
if not self.IsActive():
return False
@@ -311,6 +356,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 +456,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,13 +482,207 @@ 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.slack > 0:
+ print 'Adding slack time...'
+
+ slack_evs = []
+ for group in notegroups:
+ for ns in group.streams:
+ for dev in ns.history:
+ dev.duration += options.slack
+ slack_evs.append(dev)
+
+ print 'Resorting all streams...'
+ for group in notegroups:
+ group.streams = []
+
+ for dev in slack_evs:
+ for group in notegroups:
+ if not group.filter(dev):
+ continue
+ for ns in group.streams:
+ if dev.abstime >= ns.history[-1].abstime + ns.history[-1].duration:
+ ns.history.append(dev)
+ break
+ else:
+ group.streams.append(NoteStream())
+ group.streams[-1].history.append(dev)
+ break
+ else:
+ print 'WARNING: No stream accepts event', dev
+
+ 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
+ mwamp = float(dev.modwheel) / 0x3FFF
+ dt = 0.0
+ origtime = dev.abstime
+ events = []
+ while dt < dev.duration:
+ dev.abstime = origtime + dt
+ if options.modcont:
+ 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))
+ dt += options.modres
+ ns.history[i:i+1] = events
+ i += len(events)
+ ev_cnt += len(events)
+ if options.verbose:
+ print 'Event', i, 'note', dev, 'in group', group.name, 'resolved to', len(events), 'events'
+ if options.debug:
+ for ev in events:
+ print '\t', ev
+ else:
+ i += 1
+ print '...resolved', ev_cnt, 'events'
+
+ if options.stringres:
+ print 'Resolving string models...'
+ st_cnt = sum(sum(len(ns.history) for ns in group.streams) for group in notegroups)
+ in_cnt = 0
+ ex_cnt = 0
+ ev_cnt = 0
+ dev_grps = []
+ for group in notegroups:
+ for ns in group.streams:
+ i = 0
+ while i < len(ns.history):
+ dev = ns.history[i]
+ ntime = float('inf')
+ if i + 1 < len(ns.history):
+ ntime = ns.history[i+1].abstime
+ dt = 0.0
+ ampf = 1.0
+ origtime = dev.abstime
+ 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))
+ if len(events) > options.stringmax:
+ print 'WARNING: Exceeded maximum string model events for event', i
+ if options.verbose:
+ print 'Final ampf', ampf, 'dt', dt
+ break
+ ampf *= options.stringrateon ** options.stringres
+ dt += options.stringres
+ in_cnt += 1
+ 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))
+ if len(events) > options.stringmax:
+ print 'WARNING: Exceeded maximum string model events for event', i
+ if options.verbose:
+ print 'Final ampf', ampf, 'dt', dt
+ break
+ ampf *= options.stringrateoff ** options.stringres
+ dt += options.stringres
+ ex_cnt += 1
+ if events:
+ for j in xrange(len(events) - 1):
+ cur, next = events[j], events[j + 1]
+ if abs(cur.abstime + cur.duration - next.abstime) > options.epsilon:
+ print 'WARNING: String model events cur: ', cur, 'next:', next, 'have gap/overrun of', next.abstime - (cur.abstime + cur.duration)
+ dev_grps.append(events)
+ else:
+ print 'WARNING: Event', i, 'note', dev, ': No events?'
+ if options.verbose:
+ print 'Event', i, 'note', dev, 'in group', group.name, 'resolved to', len(events), 'events'
+ if options.debug:
+ for ev in events:
+ print '\t', ev
+ i += 1
+ ev_cnt += len(events)
+ print '...resolved', ev_cnt, 'events (+', ev_cnt - st_cnt, ',', in_cnt, 'inside', ex_cnt, 'extra), resorting streams...'
+ for group in notegroups:
+ group.streams = []
+
+ dev_grps.sort(key = lambda evg: evg[0].abstime)
+ for devgr in dev_grps:
+ dev = devgr[0]
+ for group in notegroups:
+ if group.filter(dev):
+ grp = group
+ break
+ else:
+ grp = NSGroup()
+ notegroups.append(grp)
+ for ns in grp.streams:
+ if not ns.history:
+ ns.history.extend(devgr)
+ break
+ last = ns.history[-1]
+ if dev.abstime >= last.abstime + last.duration - 1e-3:
+ ns.history.extend(devgr)
+ break
+ else:
+ ns = NoteStream()
+ grp.streams.append(ns)
+ ns.history.extend(devgr)
+ scnt = 0
+ for group in notegroups:
+ for ns in group.streams:
+ scnt += 1
+ print 'Final sort:', len(notegroups), 'groups with', scnt, 'streams'
+
+ if not options.keepempty:
+ print 'Culling empty events...'
+ ev_cnt = 0
+ 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]
+ ev_cnt += 1
+ else:
+ i += 1
+ print '...culled', ev_cnt, 'events'
if options.verbose:
print 'Final group mappings:'
for group in notegroups:
print ('<anonymous>' if group.name is None else group.name), '<=', '(', len(group.streams), 'streams)'
+ print 'Final volume resolution...'
+ for group in notegroups:
+ for ns in group.streams:
+ for ev in ns.history:
+ t, vol = sorted(filter(lambda pair: pair[0] <= ev.abstime, vol_at[ev.tidx][ev.ev.channel].items()), key=lambda pair: pair[0])[-1]
+ ev.ampl *= (float(vol) / 0x3FFF) ** options.vol_pow
+
+ print 'Checking consistency...'
+ for group in notegroups:
+ if options.verbose:
+ print 'Group', '<None>' if group.name is None else group.name, 'with', len(group.streams), 'streams...',
+ ecnt = 0
+ for ns in group.streams:
+ for i in xrange(len(ns.history) - 1):
+ cur, next = ns.history[i], ns.history[i + 1]
+ if cur.abstime + cur.duration > next.abstime + options.epsilon:
+ print 'WARNING: event', i, 'collides with next event (@', cur.abstime, '+', cur.duration, 'next @', next.abstime, ';', next.abstime - (cur.abstime + cur.duration), 'overlap)'
+ ecnt += 1
+ if cur.abstime > next.abstime:
+ print 'WARNING: event', i + 1, 'out of sort order (@', cur.abstime, 'next @', next.abstime, ';', cur.abstime - next.abstime, 'underlap)'
+ ecnt += 1
+ if options.verbose:
+ if ecnt > 0:
+ print '...', ecnt, 'errors occured'
+ else:
+ print 'ok'
+
print 'Generated %d streams in %d groups'%(sum(map(lambda x: len(x.streams), notegroups)), len(notegroups))
print 'Playtime:', lastabstime, 'seconds'
@@ -455,13 +713,20 @@ 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))
-
- ivtext = ET.SubElement(ivstreams, 'stream', type='text')
- for tev in textstream:
- ivev = ET.SubElement(ivtext, 'text', time=str(tev.abstime), type=type(tev.ev).__name__, text=tev.ev.text)
+ ivnote.set('dur', str(note.real_duration))
+
+ if not options.no_text:
+ ivtext = ET.SubElement(ivstreams, 'stream', type='text')
+ for tev in textstream:
+ text = tev.ev.text
+ try:
+ text = text.decode('utf8')
+ except UnicodeDecodeError:
+ text = 'base64:' + text.encode('base64')
+ ivev = ET.SubElement(ivtext, 'text', time=str(tev.abstime), type=type(tev.ev).__name__, text=text)
ivaux = ET.SubElement(ivstreams, 'stream')
ivaux.set('type', 'aux')
@@ -474,5 +739,9 @@ for fname in args:
ivev.set('time', str(mev.abstime))
ivev.set('data', repr(fw.encode_midi_event(mev.ev)))
+ ivargs = ET.SubElement(ivmeta, 'args')
+ ivargs.text = ' '.join('%r' % (i,) for i in sys.argv[1:])
+
print 'Done.'
- open(os.path.splitext(os.path.basename(fname))[0]+'.iv', 'w').write(ET.tostring(iv))
+ txt = ET.tostring(iv, 'UTF-8')
+ open(os.path.splitext(os.path.basename(fname))[0]+'.iv', 'wb').write(txt)
diff --git a/mktune.py b/mktune.py
new file mode 100644
index 0000000..57715b9
--- /dev/null
+++ b/mktune.py
@@ -0,0 +1,63 @@
+from xml.etree import ElementTree as ET
+import optparse
+
+parser = optparse.OptionParser()
+parser.add_option('-t', '--tempo', dest='tempo', type='float', help='Tempo (in BPM)')
+parser.add_option('-r', '--resolution', dest='resolution', type='float', help='Approximate resolution in seconds (overrides tempo)')
+parser.add_option('-f', '--float', dest='float', action='store_true', help='Allow floating point representations on output')
+parser.add_option('-T', '--transpose', dest='transpose', type='float', help='Transpose by this many semitones')
+parser.set_defaults(tempo=60000, resolution=None, transpose=0)
+options, args = parser.parse_args()
+
+maybe_int = int
+if options.float:
+ maybe_int = float
+
+class Note(object):
+ def __init__(self, time, dur, pitch, ampl):
+ self.time = time
+ self.dur = dur
+ self.pitch = pitch
+ self.ampl = ampl
+
+if options.resolution is not None:
+ options.tempo = 60.0 / options.resolution
+
+options.tempo = maybe_int(options.tempo)
+
+def to_beats(tm):
+ return options.tempo * tm / 60.0
+
+for fname in args:
+ try:
+ iv = ET.parse(fname).getroot()
+ except IOError:
+ import traceback
+ traceback.print_exc()
+ print fname, ': Bad file'
+ continue
+
+ print options.tempo,
+
+ ns = iv.find('./streams/stream[@type="ns"]')
+ prevn = None
+ for note in ns.findall('note'):
+ n = Note(
+ float(note.get('time')),
+ float(note.get('dur')),
+ float(note.get('pitch')) + options.transpose,
+ float(note.get('ampl', float(note.get('vel', 127.0)) / 127.0)),
+ )
+ if prevn is not None:
+ rtime = to_beats(n.time - (prevn.time + prevn.dur))
+ if rtime >= 1:
+ print 0, maybe_int(rtime),
+ ntime = to_beats(prevn.dur)
+ if ntime < 1 and not options.float:
+ ntime = 1
+ print maybe_int(440.0 * 2**((prevn.pitch-69)/12.0)), maybe_int(ntime),
+ prevn = n
+ ntime = to_beats(n.dur)
+ if ntime < 1 and not options.float:
+ ntime = 1
+ print int(440.0 * 2**((n.pitch-69)/12.0)), int(ntime),
diff --git a/packet.py b/packet.py
index aefa758..45308ce 100644
--- a/packet.py
+++ b/packet.py
@@ -13,18 +13,23 @@ 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)
+ PCM = 5 # 16 samples, encoded S16_LE
def itos(i):
- return struct.pack('>L', i)
+ return struct.pack('>L', i).rstrip('\0')
def stoi(s):
- return struct.unpack('>L', s)[0]
+ return struct.unpack('>L', s.ljust(4, '\0'))[0]
+
+OBLIGATE_POLYPHONE = 0xffffffff
diff --git a/shiv.py b/shiv.py
index ac6e2b1..e8cc37d 100644
--- a/shiv.py
+++ b/shiv.py
@@ -8,6 +8,7 @@ import math
parser = optparse.OptionParser()
parser.add_option('-n', '--number', dest='number', action='store_true', help='Show number of tracks')
parser.add_option('-g', '--groups', dest='groups', action='store_true', help='Show group names')
+parser.add_option('-G', '--group', dest='group', action='append', help='Only compute for this group (may be specified multiple times)')
parser.add_option('-N', '--notes', dest='notes', action='store_true', help='Show number of notes')
parser.add_option('-M', '--notes-stream', dest='notes_stream', action='store_true', help='Show notes per stream')
parser.add_option('-m', '--meta', dest='meta', action='store_true', help='Show meta track information')
@@ -23,11 +24,29 @@ parser.add_option('-x', '--aux', dest='aux', action='store_true', help='Show inf
parser.add_option('-a', '--almost-all', dest='almost_all', action='store_true', help='Show useful information')
parser.add_option('-A', '--all', dest='all', action='store_true', help='Show everything')
+parser.add_option('-t', '--total', dest='total', action='store_true', help='Make cross-file totals')
-parser.set_defaults(height=20)
+parser.set_defaults(height=20, group=[])
options, args = parser.parse_args()
+if not any((
+ options.number,
+ options.groups,
+ options.notes,
+ options.notes_stream,
+ options.histogram,
+ options.vel_hist,
+ options.duration,
+ options.duty_cycle,
+ options.aux,
+ options.meta,
+ options.histogram_tracks,
+ options.vel_hist_tracks,
+)):
+ print 'No computations specified! Assuming you meant --almost-all...'
+ options.almost_all = True
+
if options.almost_all or options.all:
options.number = True
options.groups = True
@@ -65,6 +84,7 @@ else:
def show_hist(values, height=None):
if not values:
print '{empty histogram}'
+ return
if height is None:
height = options.height
xs, ys = values.keys(), values.values()
@@ -85,16 +105,29 @@ def show_hist(values, height=None):
print COL.YELLOW + '\t ' + ''.join([s[i] if len(s) > i else ' ' for s in xcs]) + COL.NONE
print
+if options.total:
+ tot_note_cnt = 0
+ max_note_cnt = 0
+ tot_pitches = {}
+ tot_velocities = {}
+ tot_dur = 0
+ max_dur = 0
+ tot_streams = 0
+ max_streams = 0
+ tot_notestreams = 0
+ max_notestreams = 0
+ tot_groups = {}
+
for fname in args:
+ print
+ print 'File :', fname
try:
iv = ET.parse(fname).getroot()
- except IOError:
+ except Exception:
import traceback
traceback.print_exc()
print 'Bad file :', fname, ', skipping...'
continue
- print
- print 'File :', fname
print '\t<computing...>'
if options.meta:
@@ -115,10 +148,19 @@ for fname in args:
streams = iv.findall('./streams/stream')
notestreams = [s for s in streams if s.get('type') == 'ns']
auxstreams = [s for s in streams if s.get('type') == 'aux']
+ if options.group:
+ print 'NOTE: Restricting results to groups', options.group, 'as requested'
+ notestreams = [ns for ns in notestreams if ns.get('group', '<anonymous>') in options.group]
+
if options.number:
print 'Stream count:'
print '\tNotestreams:', len(notestreams)
print '\tTotal:', len(streams)
+ if options.total:
+ tot_streams += len(streams)
+ max_streams = max(max_streams, len(streams))
+ tot_notestreams += len(notestreams)
+ max_notestreams = max(max_notestreams, len(notestreams))
if not (options.groups or options.notes or options.histogram or options.histogram_tracks or options.vel_hist or options.vel_hist_tracks or options.duration or options.duty_cycle or options.aux):
continue
@@ -128,6 +170,8 @@ for fname in args:
for s in notestreams:
group = s.get('group', '<anonymous>')
groups[group] = groups.get(group, 0) + 1
+ if options.total:
+ tot_groups[group] = tot_groups.get(group, 0) + 1
print 'Groups:'
for name, cnt in groups.iteritems():
print '\t{} ({} streams)'.format(name, cnt)
@@ -175,26 +219,35 @@ for fname in args:
notes = stream.findall('note')
for note in notes:
pitch = float(note.get('pitch'))
- vel = int(note.get('vel'))
+ ampl = int(127 * float(note.get('ampl', float(note.get('vel', 127.0)) / 127.0)))
time = float(note.get('time'))
dur = float(note.get('dur'))
if options.notes:
note_cnt += 1
+ if options.total:
+ tot_note_cnt += 1
if options.notes_stream:
notes_stream[sidx] += 1
if options.histogram:
pitches[pitch] = pitches.get(pitch, 0) + 1
+ if options.total:
+ tot_pitches[pitch] = tot_pitches.get(pitch, 0) + 1
if options.histogram_tracks:
pitch_tracks[sidx][pitch] = pitch_tracks[sidx].get(pitch, 0) + 1
if options.vel_hist:
- velocities[vel] = velocities.get(vel, 0) + 1
+ velocities[ampl] = velocities.get(ampl, 0) + 1
+ if options.total:
+ tot_velocities[ampl] = tot_velocities.get(ampl, 0) + 1
if options.vel_hist_tracks:
- velocities_tracks[sidx][vel] = velocities_tracks[sidx].get(vel, 0) + 1
+ velocities_tracks[sidx][ampl] = velocities_tracks[sidx].get(ampl, 0) + 1
if (options.duration or options.duty_cycle) and time + dur > max_dur:
max_dur = time + dur
if options.duty_cycle:
cum_dur[sidx] += dur
+ if options.notes and options.total:
+ max_note_cnt = max(max_note_cnt, note_cnt)
+
if options.histogram_tracks:
for sidx, hist in enumerate(pitch_tracks):
print 'Stream {} (group {}) pitch histogram:'.format(sidx, notestreams[sidx].get('group', '<anonymous>'))
@@ -219,3 +272,27 @@ for fname in args:
show_hist(velocities)
if options.duration:
print 'Playing duration: {}'.format(max_dur)
+
+if options.total:
+ print 'Totals:'
+ if options.number:
+ print '\tTotal streams:', tot_streams
+ print '\tMax streams:', max_streams
+ print '\tTotal notestreams:', tot_notestreams
+ print '\tMax notestreams:', max_notestreams
+ print
+ if options.notes:
+ print '\tTotal notes:', tot_note_cnt
+ print '\tMax notes:', max_note_cnt
+ print
+ if options.groups:
+ print '\tGroups:'
+ for grp, cnt in tot_groups.iteritems():
+ print '\t\t', grp, ':', cnt
+ print
+ if options.histogram:
+ print 'Overall pitch histogram:'
+ show_hist(tot_pitches)
+ if options.vel_hist:
+ print 'Overall velocity histogram:'
+ show_hist(tot_velocities)