From 3e7481519e35c670cbf75320e6a1bd4485e42400 Mon Sep 17 00:00:00 2001 From: Grissess Date: Mon, 15 Jun 2015 01:37:37 -0400 Subject: Major functionality enhancements and bugfixes --- broadcast.py | 49 +++++++- client.c | 7 +- client.py | 68 +++++++++-- mkiv.py | 381 +++++++++++++++++++++++++++++++++++++++-------------------- 4 files changed, 353 insertions(+), 152 deletions(-) diff --git a/broadcast.py b/broadcast.py index aaf1276..7bd7bd0 100644 --- a/broadcast.py +++ b/broadcast.py @@ -4,9 +4,35 @@ import struct import time import xml.etree.ElementTree as ET import threading +import optparse from packet import Packet, CMD, itos +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('-q', '--quit', dest='quit', action='store_true', help='Instruct all clients to quit') +parser.add_option('-f', '--factor', dest='factor', type='int', help='Rescale time by this factor (0 2: factor = float(sys.argv[2]) @@ -19,6 +45,8 @@ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) clients = [] +uid_groups = {} +type_groups = {} s.sendto(str(Packet(CMD.PING)), ('255.255.255.255', PORT)) s.settimeout(0.5) @@ -37,15 +65,26 @@ for cl in clients: data, _ = s.recvfrom(4096) pkt = Packet.FromStr(data) print 'ports', pkt.data[0], - print 'type', itos(pkt.data[1]), - print 'uid', ''.join([itos(i) for i in pkt.data[2:]]).rstrip('\x00') - if sys.argv[1] == '-t': + 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) + if options.test: s.sendto(str(Packet(CMD.PLAY, 0, 250000, 440, 255)), cl) time.sleep(0.25) s.sendto(str(Packet(CMD.PLAY, 0, 250000, 880, 255)), cl) - if sys.argv[1] == '-q': + if options.quit: s.sendto(str(Packet(CMD.QUIT)), cl) +if options.test or options.quit: + print uid_groups + print type_groups + exit() + try: iv = ET.parse(sys.argv[1]).getroot() except IOError: @@ -53,8 +92,10 @@ except IOError: exit() notestreams = iv.findall("./streams/stream[@type='ns']") +groups = set([ns.get('group') for ns in notestreams if 'group' in ns.keys()]) print len(notestreams), 'notestreams' print len(clients), 'clients' +print len(groups), 'groups' class NSThread(threading.Thread): def run(self): diff --git a/client.c b/client.c index 830ef19..a7a5472 100644 --- a/client.c +++ b/client.c @@ -90,8 +90,8 @@ int main(int argc, char **argv) { break; case CMD_CAPS: - cmd.data[0] = 1; - cmd.data[1] = ident; + cmd.data[0] = htonl(1); + cmd.data[1] = htonl(ident); for(i = 0; i < 6 * sizeof(int); i++) { if(argc > 1 && i < len_uid) { cmd.string[i+8] = argv[1][i]; @@ -99,9 +99,6 @@ int main(int argc, char **argv) { cmd.string[i+8] = '\0'; } } - for(i = 0; i < 8; i++) { - cmd.data[i] = htonl(cmd.data[i]); - } sendto(sock, &cmd, sizeof(cmd), 0, (struct sockaddr *) &remote, rlen); break; diff --git a/client.py b/client.py index fb3db19..14cccef 100644 --- a/client.py +++ b/client.py @@ -8,21 +8,28 @@ import time import math import struct import socket +import optparse from packet import Packet, CMD, stoi -PORT = 13676 +parser = optparse.OptionParser() +parser.add_option('-t', '--test', dest='test', action='store_true', help='Play a test sequence (440,,880,440), then exit') +parser.add_option('-g', '--generator', dest='generator', default='math.sin', help='Set the generator (to a Python expression)') +parser.add_option('-u', '--uid', dest='uid', default='', help='Set the UID (identifier) of this client in the network') +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') + +options, args = parser.parse_args() + +PORT = options.port STREAMS = 1 IDENT = 'TONE' -if len(sys.argv) > 1: - UID = sys.argv[1].ljust(24, '\x00') -else: - UID = '\x00'*24 +UID = options.uid LAST_SAMP = 0 FREQ = 0 PHASE = 0 -RATE = 44100 +RATE = options.rate FPB = 64 Z_SAMP = '\x00\x00\x00\x00' @@ -30,24 +37,48 @@ MAX = 0x7fffffff AMP = MAX MIN = -0x80000000 +def lin_interp(frm, to, p): + return p*to + (1-p)*frm + +# Generator functions--should be cyclic within [0, 2*math.pi) and return [-1, 1] + +def tri_wave(theta): + if theta < math.pi/2: + return lin_interp(0, 1, theta/(math.pi/2)) + elif theta < 3*math.pi/2: + return lin_interp(1, -1, (theta-math.pi/2)/math.pi) + else: + return lin_interp(-1, 0, (theta-3*math.pi/2)/(math.pi/2)) + +def square_wave(theta): + if theta < math.pi: + return 1 + else: + return -1 + +#generator = math.sin +#generator = tri_wave +#generator = square_wave +generator = eval(options.generator) + def sigalrm(sig, frm): global FREQ FREQ = 0 -def lin_interp(frm, to, cnt): +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(p*to + (1-p)*frm) + samps[i] = int(lin_interp(frm, to, p)) return samps -def sine(freq, phase, cnt): +def samps(freq, phase, cnt): global RATE, AMP samps = [0]*cnt for i in xrange(cnt): - samps[i] = int(AMP * math.sin(phase + 2 * math.pi * freq * i / RATE)) - return samps, phase + 2 * math.pi * freq * cnt / RATE + samps[i] = int(AMP * 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) @@ -58,16 +89,27 @@ def gen_data(data, frames, time, status): PHASE = 0 if LAST_SAMP == 0: return (Z_SAMP*frames, pyaudio.paContinue) - fdata = lin_interp(LAST_SAMP, 0, frames) + fdata = lin_seq(LAST_SAMP, 0, frames) LAST_SAMP = fdata[-1] return (to_data(fdata), pyaudio.paContinue) - fdata, PHASE = sine(FREQ, PHASE, frames) + fdata, PHASE = samps(FREQ, PHASE, frames) LAST_SAMP = fdata[-1] return (to_data(fdata), pyaudio.paContinue) pa = pyaudio.PyAudio() stream = pa.open(rate=RATE, channels=1, format=pyaudio.paInt32, output=True, frames_per_buffer=FPB, stream_callback=gen_data) +if options.test: + FREQ = 440 + time.sleep(1) + FREQ = 0 + time.sleep(1) + FREQ = 880 + time.sleep(1) + FREQ = 440 + time.sleep(2) + exit() + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(('', PORT)) diff --git a/mkiv.py b/mkiv.py index bddbe8b..0b6577e 100644 --- a/mkiv.py +++ b/mkiv.py @@ -16,141 +16,262 @@ import xml.etree.ElementTree as ET import midi import sys import os +import optparse -pat = midi.read_midifile(sys.argv[1]) -iv = ET.Element('iv') -iv.set('version', '1') -iv.set('src', os.path.basename(sys.argv[1])) +TRACKS = object() + +parser = optparse.OptionParser() +parser.add_option('-s', '--channel-split', dest='chansplit', action='store_true', help='Split MIDI channels into independent tracks (as far as -T is concerned)') +parser.add_option('-S', '--split-out', dest='chansfname', help='Store the split-format MIDI back into the specified file') +parser.add_option('-c', '--preserve-channels', dest='chanskeep', action='store_true', help='Keep the channel number when splitting channels to tracks (default is to set it to 1)') +parser.add_option('-T', '--track-split', dest='tracks', action='append_const', const=TRACKS, help='Ensure all tracks are on non-mutual streams') +parser.add_option('-t', '--track', dest='tracks', action='append', help='Reserve an exclusive set of streams for certain conditions (try --help-conds)') +parser.add_option('--help-conds', dest='help_conds', action='store_true', help='Print help on filter conditions for streams') +parser.set_defaults(tracks=[]) +options, args = parser.parse_args() + +if options.help_conds: + print '''Filter conditions are used to route events to groups of streams. + +Every filter is an expression; internally, this expression is evaluated as the body of a "lambda ev: ". +The "ev" object will be a MergeEvent with the following properties: +-ev.tidx: the originating track index (starting at 0) +-ev.abstime: the real time in seconds of this event relative to the beginning of playback +-ev.ev: a midi.NoteOnEvent: + -ev.ev.pitch: the MIDI pitch + -ev.ev.velocity: the MIDI velocity + +Specifying a -t = will group all streams under a filter; if the part is omitted, no group will be added. +For example: + + mkiv -t bass=ev.ev.pitch<35 -t treble=ev.ev.pitch>75 -T -t ev.abstime<10 + +will cause these groups to be made: +-A group "bass" with all notes with pitch less than 35; +-Of those not in "bass", a group in "treble" with pitch>75; +-Of what is not yet consumed, a series of groups "trkN" where N is the track index (starting at 0), which consumes the rest. +-An (unfortunately empty) unnamed group with events prior to ten real seconds. + +As can be seen, order of specification is important. Equally important is the location of -T, which should be at the end. + +NoteOffEvents are always matched to the stream which has their corresponding NoteOnEvent (in track and pitch), and so are +not affected or observed by filters. + +If the filters specified are not a complete cover, an anonymous group will be created with no filter to contain the rest. If +it is desired to force this group to have a name, use -t =True.''' + exit() + +if not args: + parser.print_usage() + exit() + +for fname in args: + pat = midi.read_midifile(fname) + iv = ET.Element('iv') + iv.set('version', '1') + iv.set('src', os.path.basename(fname)) + print fname, ': MIDI format,', len(pat), 'tracks' + + if options.chansplit: + print 'Splitting channels...' + old_pat = pat + pat = midi.Pattern(resolution=old_pat.resolution) + for track in old_pat: + chan_map = {} + for ev in track: + if isinstance(ev, midi.Event): + if options.chanskeep: + newev = ev.copy() + else: + newev = ev.copy(channel=1) + chan_map.setdefault(ev.channel, midi.Track()).append(newev) + else: # MetaEvent + for trk in chan_map.itervalues(): + trk.append(ev) + items = chan_map.items() + items.sort(key=lambda pair: pair[0]) + for chn, trk in items: + pat.append(trk) + print 'Split', len(old_pat), 'tracks into', len(pat), 'tracks by channel' + + if options.chansfname: + midi.write_midifile(options.chansfname, pat) ##### Merge events from all tracks into one master list, annotated with track and absolute times ##### -print 'Merging events...' - -class MergeEvent(object): - __slots__ = ['ev', 'tidx', 'abstime'] - def __init__(self, ev, tidx, abstime): - self.ev = ev - self.tidx = tidx - self.abstime = abstime - def __repr__(self): - return ''%(self.ev, self.tidx, self.abstime) - -events = [] -bpm_at = {0: 120} - -for tidx, track in enumerate(pat): - abstime = 0 - absticks = 0 - for ev in track: - if isinstance(ev, midi.SetTempoEvent): - absticks += ev.tick - bpm_at[absticks] = ev.bpm - else: - if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0: - ev.__class__ = midi.NoteOffEvent #XXX Oww - bpm = filter(lambda pair: pair[0] <= absticks, bpm_at.items())[-1][1] - abstime += (60.0 * ev.tick) / (bpm * pat.resolution) - absticks += ev.tick - events.append(MergeEvent(ev, tidx, abstime)) - -print 'Sorting events...' - -events.sort(key = lambda ev: ev.abstime) + print 'Merging events...' + + class MergeEvent(object): + __slots__ = ['ev', 'tidx', 'abstime'] + def __init__(self, ev, tidx, abstime): + self.ev = ev + self.tidx = tidx + self.abstime = abstime + def __repr__(self): + return ''%(self.ev, self.tidx, self.abstime) + + events = [] + bpm_at = {0: 120} + + for tidx, track in enumerate(pat): + abstime = 0 + absticks = 0 + for ev in track: + if isinstance(ev, midi.SetTempoEvent): + absticks += ev.tick + bpm_at[absticks] = ev.bpm + else: + if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0: + ev.__class__ = midi.NoteOffEvent #XXX Oww + bpm = filter(lambda pair: pair[0] <= absticks, bpm_at.items())[-1][1] + abstime += (60.0 * ev.tick) / (bpm * pat.resolution) + absticks += ev.tick + events.append(MergeEvent(ev, tidx, abstime)) + + print 'Sorting events...' + + events.sort(key = lambda ev: ev.abstime) ##### Use merged events to construct a set of streams with non-overlapping durations ##### -print 'Generating streams...' - -class DurationEvent(MergeEvent): - __slots__ = ['duration'] - def __init__(self, me, dur): - MergeEvent.__init__(self, me.ev, me.tidx, me.abstime) - self.duration = dur - -class NoteStream(object): - __slots__ = ['history', 'active'] - def __init__(self): - self.history = [] - self.active = None - def IsActive(self): - return self.active is not None - def Activate(self, mev): - self.active = mev - def Deactivate(self, mev): - self.history.append(DurationEvent(self.active, mev.abstime - self.active.abstime)) - self.active = None - def WouldDeactivate(self, mev): - if not self.IsActive(): - return False - return mev.ev.pitch == self.active.ev.pitch and mev.tidx == self.active.tidx - -notestreams = [] -auxstream = [] - -for mev in events: - if isinstance(mev.ev, midi.NoteOnEvent): - for stream in notestreams: - if not stream.IsActive(): - stream.Activate(mev) - break - else: - stream = NoteStream() - notestreams.append(stream) - stream.Activate(mev) - elif isinstance(mev.ev, midi.NoteOffEvent): - for stream in notestreams: - if stream.WouldDeactivate(mev): - stream.Deactivate(mev) - break - else: - print 'WARNING: Did not match %r with any stream deactivation.'%(mev,) - else: - auxstream.append(mev) - -lastabstime = events[-1].abstime - -for ns in notestreams: - if not ns: - print 'WARNING: Active notes at end of playback.' - ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime)) - -print 'Generated %d streams'%(len(notestreams),) + print 'Generating streams...' + + class DurationEvent(MergeEvent): + __slots__ = ['duration'] + def __init__(self, me, dur): + MergeEvent.__init__(self, me.ev, me.tidx, me.abstime) + self.duration = dur + + class NoteStream(object): + __slots__ = ['history', 'active'] + def __init__(self): + self.history = [] + self.active = None + def IsActive(self): + return self.active is not None + def Activate(self, mev): + self.active = mev + def Deactivate(self, mev): + self.history.append(DurationEvent(self.active, mev.abstime - self.active.abstime)) + self.active = None + def WouldDeactivate(self, mev): + if not self.IsActive(): + return False + return mev.ev.pitch == self.active.ev.pitch and mev.tidx == self.active.tidx + + class NSGroup(object): + __slots__ = ['streams', 'filter', 'name'] + def __init__(self, filter=None, name=None): + self.streams = [] + self.filter = (lambda mev: True) if filter is None else filter + self.name = name + def Accept(self, mev): + if not self.filter(mev): + return False + for stream in self.streams: + if not stream.IsActive(): + stream.Activate(mev) + break + else: + stream = NoteStream() + self.streams.append(stream) + stream.Activate(mev) + return True + + notegroups = [] + auxstream = [] + + for spec in options.tracks: + if spec is TRACKS: + for tidx in xrange(len(pat)): + notegroups.append(NSGroup(filter = lambda mev, tidx=tidx: mev.tidx == tidx, name = 'trk%d'%(tidx,))) + else: + if '=' in spec: + name, _, spec = spec.partition('=') + else: + name = None + notegroups.append(NSGroup(filter = eval("lambda ev: "+spec), name = name)) + + print 'Initial group mappings:' + for group in notegroups: + print ('' if group.name is None else group.name), '<=', group.filter + + for mev in events: + if isinstance(mev.ev, midi.NoteOnEvent): + for group in notegroups: + if group.Accept(mev): + break + else: + group = NSGroup() + group.Accept(mev) + notegroups.append(group) + elif isinstance(mev.ev, midi.NoteOffEvent): + for group in notegroups: + found = False + for stream in group.streams: + if stream.WouldDeactivate(mev): + stream.Deactivate(mev) + found = True + break + if found: + break + else: + print 'WARNING: Did not match %r with any stream deactivation.'%(mev,) + else: + auxstream.append(mev) + + lastabstime = events[-1].abstime + + for group in notegroups: + 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)) + + print 'Final group mappings:' + for group in notegroups: + print ('' if group.name is None else group.name), '<=', group.filter, '(', len(group.streams), 'streams)' + + print 'Generated %d streams in %d groups'%(sum(map(lambda x: len(x.streams), notegroups)), len(notegroups)) ##### Write to XML and exit ##### -ivmeta = ET.SubElement(iv, 'meta') -ivbpms = ET.SubElement(ivmeta, 'bpms') -abstime = 0 -prevticks = 0 -prev_bpm = 120 -for absticks, bpm in sorted(bpm_at.items(), key = lambda pair: pair[0]): - abstime += ((absticks - prevticks) * 60.0) / (prev_bpm * pat.resolution) - prevticks = absticks - ivbpm = ET.SubElement(ivbpms, 'bpm') - ivbpm.set('bpm', str(bpm)) - ivbpm.set('ticks', str(absticks)) - ivbpm.set('time', str(abstime)) - -ivstreams = ET.SubElement(iv, 'streams') - -for ns in notestreams: - ivns = ET.SubElement(ivstreams, 'stream') - ivns.set('type', 'ns') - for note in ns.history: - ivnote = ET.SubElement(ivns, 'note') - ivnote.set('pitch', str(note.ev.pitch)) - ivnote.set('vel', str(note.ev.velocity)) - ivnote.set('time', str(note.abstime)) - ivnote.set('dur', str(note.duration)) - -ivaux = ET.SubElement(ivstreams, 'stream') -ivaux.set('type', 'aux') - -fw = midi.FileWriter() -fw.RunningStatus = None # XXX Hack - -for mev in auxstream: - ivev = ET.SubElement(ivaux, 'ev') - ivev.set('time', str(mev.abstime)) - ivev.set('data', repr(fw.encode_midi_event(mev.ev))) - -print 'Done.' -open(os.path.splitext(os.path.basename(sys.argv[1]))[0]+'.iv', 'w').write(ET.tostring(iv)) + ivmeta = ET.SubElement(iv, 'meta') + ivbpms = ET.SubElement(ivmeta, 'bpms') + abstime = 0 + prevticks = 0 + prev_bpm = 120 + for absticks, bpm in sorted(bpm_at.items(), key = lambda pair: pair[0]): + abstime += ((absticks - prevticks) * 60.0) / (prev_bpm * pat.resolution) + prevticks = absticks + ivbpm = ET.SubElement(ivbpms, 'bpm') + ivbpm.set('bpm', str(bpm)) + ivbpm.set('ticks', str(absticks)) + ivbpm.set('time', str(abstime)) + + ivstreams = ET.SubElement(iv, 'streams') + + for group in notegroups: + for ns in group.streams: + ivns = ET.SubElement(ivstreams, 'stream') + ivns.set('type', 'ns') + if group.name is not None: + ivns.set('group', group.name) + for note in ns.history: + ivnote = ET.SubElement(ivns, 'note') + ivnote.set('pitch', str(note.ev.pitch)) + ivnote.set('vel', str(note.ev.velocity)) + ivnote.set('time', str(note.abstime)) + ivnote.set('dur', str(note.duration)) + + ivaux = ET.SubElement(ivstreams, 'stream') + ivaux.set('type', 'aux') + + fw = midi.FileWriter() + fw.RunningStatus = None # XXX Hack + + for mev in auxstream: + ivev = ET.SubElement(ivaux, 'ev') + ivev.set('time', str(mev.abstime)) + ivev.set('data', repr(fw.encode_midi_event(mev.ev))) + + print 'Done.' + open(os.path.splitext(os.path.basename(fname))[0]+'.iv', 'w').write(ET.tostring(iv)) -- cgit v1.2.3-70-g09d2