From bb38c09530d7e66182c0db5205c15b143f3d5a9b Mon Sep 17 00:00:00 2001 From: Grissess Date: Tue, 14 Jun 2016 02:44:27 -0400 Subject: Modwheel stuff, floating-point amplitude --- mkiv.py | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 98 insertions(+), 21 deletions(-) (limited to 'mkiv.py') diff --git a/mkiv.py b/mkiv.py index 717220c..a3bc42a 100644 --- a/mkiv.py +++ b/mkiv.py @@ -32,8 +32,14 @@ parser.add_option('-f', '--fuckit', dest='fuckit', action='store_true', help='Us parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Be verbose; show important parts about the MIDI scheduling process') parser.add_option('-d', '--debug', dest='debug', action='store_true', help='Debugging output; show excessive output about the MIDI scheduling process (please use less or write to a file)') parser.add_option('-D', '--deviation', dest='deviation', type='int', help='Amount (in semitones/MIDI pitch units) by which a fully deflected pitchbend modifies the base pitch (0 disables pitchbend processing)') +parser.add_option('-M', '--modwheel-freq-dev', dest='modfdev', type='float', help='Amount (in semitones/MIDI pitch unites) by which a fully-activated modwheel modifies the base pitch') +parser.add_option('--modwheel-freq-freq', dest='modffreq', type='float', help='Frequency of modulation periods (sinusoids) of the modwheel acting on the base pitch') +parser.add_option('--modwheel-amp-dev', dest='modadev', type='float', help='Deviation [0, 1] by which a fully-activated modwheel affects the amplitude as a factor of that amplitude') +parser.add_option('--modwheel-amp-freq', dest='modafreq', type='float', help='Frequency of modulation periods (sinusoids) of the modwheel acting on amplitude') +parser.add_option('--modwheel-res', dest='modres', type='float', help='(Fractional) seconds by which to resolve modwheel events (0 to disable)') parser.add_option('--tempo', dest='tempo', help='Adjust interpretation of tempo (try "f1"/"global", "f2"/"track")') -parser.set_defaults(tracks=[], perc='GM', deviation=2, tempo='global') +parser.add_option('-0', '--keep-empty', dest='keepempty', action='store_true', help='Keep (do not cull) events with 0 duration in the output file') +parser.set_defaults(tracks=[], perc='GM', deviation=2, tempo='global', modres=0.01, modfdev=1.0, modffreq=5.0, modadev=0.5, modafreq=5.0) options, args = parser.parse_args() if options.tempo == 'f1': options.tempo == 'global' @@ -49,6 +55,7 @@ The "ev" object will be a MergeEvent with the following properties: -ev.abstime: the real time in seconds of this event relative to the beginning of playback -ev.bank: the selected bank (all bits) -ev.prog: the selected program +-ev.mw: the modwheel value -ev.ev: a midi.NoteOnEvent: -ev.ev.pitch: the MIDI pitch -ev.ev.velocity: the MIDI velocity @@ -214,23 +221,26 @@ for fname in args: return rt class MergeEvent(object): - __slots__ = ['ev', 'tidx', 'abstime', 'bank', 'prog'] - def __init__(self, ev, tidx, abstime, bank, prog): + __slots__ = ['ev', 'tidx', 'abstime', 'bank', 'prog', 'mw'] + def __init__(self, ev, tidx, abstime, bank=0, prog=0, mw=0): self.ev = ev self.tidx = tidx self.abstime = abstime self.bank = bank self.prog = prog + self.mw = mw def copy(self, **kwargs): - args = {'ev': self.ev, 'tidx': self.tidx, 'abstime': self.abstime, 'bank': self.bank, 'prog': self.prog} + args = {'ev': self.ev, 'tidx': self.tidx, 'abstime': self.abstime, 'bank': self.bank, 'prog': self.prog, 'mw': self.mw} args.update(kwargs) return MergeEvent(**args) def __repr__(self): - return ''%(self.ev, self.tidx, self.bank, self.prog, self.abstime) + return ''%(self.ev, self.tidx, self.bank, self.prog, self.mw, self.abstime) events = [] + cur_mw = [[0 for i in range(16)] for j in range(len(pat))] cur_bank = [[0 for i in range(16)] for j in range(len(pat))] cur_prog = [[0 for i in range(16)] for j in range(len(pat))] + chg_mw = [[0 for i in range(16)] for j in range(len(pat))] chg_bank = [[0 for i in range(16)] for j in range(len(pat))] chg_prog = [[0 for i in range(16)] for j in range(len(pat))] ev_cnts = [[0 for i in range(16)] for j in range(len(pat))] @@ -253,18 +263,26 @@ for fname in args: progs.add(ev.value) chg_prog[tidx][ev.channel] += 1 elif isinstance(ev, midi.ControlChangeEvent): - if ev.control == 0: + if ev.control == 0: # Bank -- MSB cur_bank[tidx][ev.channel] = (0x3F80 & cur_bank[tidx][ev.channel]) | ev.value chg_bank[tidx][ev.channel] += 1 - elif ev.control == 32: + elif ev.control == 32: # Bank -- LSB cur_bank[tidx][ev.channel] = (0x3F & cur_bank[tidx][ev.channel]) | (ev.value << 7) chg_bank[tidx][ev.channel] += 1 + elif ev.control == 1: # ModWheel -- MSB + cur_mw[tidx][ev.channel] = (0x3F80 & cur_mw[tidx][ev.channel]) | ev.value + chg_mw[tidx][ev.channel] += 1 + elif ev.control == 33: # ModWheel -- LSB + cur_mw[tidx][ev.channel] = (0x3F & cur_mw[tidx][ev.channel]) | (ev.value << 7) + chg_mw[tidx][ev.channel] += 1 + events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel], cur_mw[tidx][ev.channel])) + ev_cnts[tidx][ev.channel] += 1 elif isinstance(ev, midi.MetaEventWithText): - events.append(MergeEvent(ev, tidx, abstime, 0, 0)) + events.append(MergeEvent(ev, tidx, abstime)) elif isinstance(ev, midi.Event): if isinstance(ev, midi.NoteOnEvent) and ev.velocity == 0: ev.__class__ = midi.NoteOffEvent #XXX Oww - events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel])) + events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel], cur_mw[tidx][ev.channel])) ev_cnts[tidx][ev.channel] += 1 if options.verbose: @@ -281,27 +299,32 @@ for fname in args: print 'Generating streams...' class DurationEvent(MergeEvent): - __slots__ = ['duration', 'pitch'] - def __init__(self, me, pitch, dur): - MergeEvent.__init__(self, me.ev, me.tidx, me.abstime, me.bank, me.prog) + __slots__ = ['duration', 'pitch', 'modwheel', 'ampl'] + def __init__(self, me, pitch, ampl, dur, modwheel=0): + MergeEvent.__init__(self, me.ev, me.tidx, me.abstime, me.bank, me.prog, me.mw) self.pitch = pitch + self.ampl = ampl self.duration = dur + self.modwheel = modwheel class NoteStream(object): - __slots__ = ['history', 'active', 'realpitch'] + __slots__ = ['history', 'active', 'bentpitch', 'modwheel'] def __init__(self): self.history = [] self.active = None - self.realpitch = None + self.bentpitch = None + self.modwheel = 0 def IsActive(self): return self.active is not None - def Activate(self, mev, realpitch = None): - if realpitch is None: - realpitch = mev.ev.pitch + def Activate(self, mev, bentpitch = None, modwheel = None): + if bentpitch is None: + bentpitch = mev.ev.pitch self.active = mev - self.realpitch = realpitch + self.bentpitch = bentpitch + if modwheel is not None: + self.modwheel = modwheel def Deactivate(self, mev): - self.history.append(DurationEvent(self.active, self.realpitch, mev.abstime - self.active.abstime)) + self.history.append(DurationEvent(self.active, self.realpitch, self.active.ev.velocity / 127.0, .abstime - self.active.abstime, self.modwheel)) self.active = None self.realpitch = None def WouldDeactivate(self, mev): @@ -311,6 +334,8 @@ for fname in args: return mev.ev.pitch == self.active.ev.pitch and mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel if isinstance(mev.ev, midi.PitchWheelEvent): return mev.tidx == self.active.tidx and mev.ev.channel == self.active.ev.channel + if isinstance(mev.ev, midi.ControlChangeEvent): + return mev.tidx == self.active.tidx and mev.ev.channel = self.active.ev.channel raise TypeError('Tried to deactivate with bad type %r'%(type(mev.ev),)) class NSGroup(object): @@ -409,6 +434,23 @@ for fname in args: print ' Group %r:'%(group.name,) for stream in group.streams: print ' Stream: %r'%(stream.active,) + elif options.modres <= 0 and isinstance(mev.ev, midi.ControlChangeEvent): + found = False + for group in notegroups: + for stream in group.streams: + if stream.WouldDeactivate(mev): + base = stream.active.copy(abstime=mev.abstime) + stream.Deactivate(mev) + stream.Activate(base, stream.bentpitch, mev.mw) + found = True + if not found: + print 'WARNING: Did not find any matching active streams for %r'%(mev,) + if options.verbose: + print ' Current state:' + for group in notegroups: + print ' Group %r:'%(group.name,) + for stream in group.streams: + print ' Stream: %r'%(stream.active,) else: auxstream.append(mev) @@ -418,7 +460,41 @@ for fname in args: for ns in group.streams: if ns.IsActive(): print 'WARNING: Active notes at end of playback.' - ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime, 0, 0)) + ns.Deactivate(MergeEvent(ns.active, ns.active.tidx, lastabstime)) + + if options.modres > 0: + print 'Resolving modwheel events...' + ev_cnt = 0 + for group in notegroups: + for ns in group.streams: + i = 0 + while i < len(ns.history): + dev = ns.history[i] + if dev.modwheel > 0: + realpitch = dev.pitch + realamp = dev.ampl + dt = 0.0 + events = [] + while dt < dev.duration: + events.append(DurationEvent(dev, realpitch + options.modfdev * math.sin(options.modffreq * (dev.abstime + dt)), realamp + options.modadev * math.sin(options.modafreq * (dev.abstime + dt)), dev.duration, dev.modwheel)) + dt += options.modres + ns.history[i:i+1] = events + i += len(events) + ev_cnt += len(events) + else: + i += 1 + print '...resolved', ev_cnt, 'events' + + if not options.keepempty: + print 'Culling empty events...' + for group in notegroups: + for ns in group.streams: + i = 0 + while i < len(ns.history): + if ns.history[i].duration == 0.0: + del ns.history[i] + else: + i += 1 if options.verbose: print 'Final group mappings:' @@ -455,7 +531,8 @@ for fname in args: for note in ns.history: ivnote = ET.SubElement(ivns, 'note') ivnote.set('pitch', str(note.pitch)) - ivnote.set('vel', str(note.ev.velocity)) + ivnote.set('vel', str(int(note.ampl * 127.0))) + ivnote.set('ampl', str(note.ampl)) ivnote.set('time', str(note.abstime)) ivnote.set('dur', str(note.duration)) -- cgit v1.2.3-70-g09d2 From 2cb9c95ea18906258a4fc2df1fe229d191ae5e31 Mon Sep 17 00:00:00 2001 From: Grissess Date: Tue, 14 Jun 2016 02:56:53 -0400 Subject: Bugfixes --- broadcast.py | 2 +- client.py | 2 +- mkiv.py | 15 ++++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) (limited to 'mkiv.py') diff --git a/broadcast.py b/broadcast.py index 1fcf5a8..91b4e71 100644 --- a/broadcast.py +++ b/broadcast.py @@ -473,7 +473,7 @@ for fname in args: for note in nsq: ttime = float(note.get('time')) pitch = float(note.get('pitch')) + options.transpose - ampl = float(note.get('ampl', note.get('vel', 127.0) / 127.0)) + 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)) diff --git a/client.py b/client.py index 1680017..766b639 100644 --- a/client.py +++ b/client.py @@ -355,7 +355,7 @@ while True: elif pkt.cmd == CMD.PLAY: dur = pkt.data[0]+pkt.data[1]/1000000.0 FREQ = pkt.data[2] - AMP = MAX * (pkt.as_float(3)) + AMP = MAX * max(min(pkt.as_float(3), 1.0), 0.0) signal.setitimer(signal.ITIMER_REAL, dur) elif pkt.cmd == CMD.CAPS: data = [0] * 8 diff --git a/mkiv.py b/mkiv.py index a3bc42a..e5d06a2 100644 --- a/mkiv.py +++ b/mkiv.py @@ -15,6 +15,7 @@ import midi import sys import os import optparse +import math TRACKS = object() PROGRAMS = object() @@ -286,9 +287,9 @@ for fname in args: ev_cnts[tidx][ev.channel] += 1 if options.verbose: - print 'Track name, event count, final banks, bank changes, final programs, program changes:' + print 'Track name, event count, final banks, bank changes, final programs, program changes, final modwheel, modwheel 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 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])) print 'All programs observed:', progs print 'Sorting events...' @@ -324,9 +325,9 @@ for fname in args: if modwheel is not None: self.modwheel = modwheel def Deactivate(self, mev): - self.history.append(DurationEvent(self.active, self.realpitch, self.active.ev.velocity / 127.0, .abstime - self.active.abstime, self.modwheel)) + self.history.append(DurationEvent(self.active, self.bentpitch, self.active.ev.velocity / 127.0, mev.abstime - self.active.abstime, self.modwheel)) self.active = None - self.realpitch = None + self.bentpitch = None def WouldDeactivate(self, mev): if not self.IsActive(): return False @@ -335,7 +336,7 @@ for fname in args: 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 + 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): @@ -434,7 +435,7 @@ 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): + elif options.modres > 0 and isinstance(mev.ev, midi.ControlChangeEvent): found = False for group in notegroups: for stream in group.streams: @@ -476,7 +477,7 @@ for fname in args: dt = 0.0 events = [] while dt < dev.duration: - events.append(DurationEvent(dev, realpitch + options.modfdev * math.sin(options.modffreq * (dev.abstime + dt)), realamp + options.modadev * math.sin(options.modafreq * (dev.abstime + dt)), dev.duration, dev.modwheel)) + events.append(DurationEvent(dev, realpitch + options.modfdev * math.sin(options.modffreq * (dev.abstime + dt)), realamp + options.modadev * (math.sin(options.modafreq * (dev.abstime + dt)) - 1.0) / 2.0, dev.duration, dev.modwheel)) dt += options.modres ns.history[i:i+1] = events i += len(events) -- cgit v1.2.3-70-g09d2 From 8028934567b4c4a4046aa4665e26c7c59b96a6c3 Mon Sep 17 00:00:00 2001 From: Grissess Date: Tue, 14 Jun 2016 03:46:38 -0400 Subject: Modwheel appears to work --- mkiv.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) (limited to 'mkiv.py') diff --git a/mkiv.py b/mkiv.py index e5d06a2..867a933 100644 --- a/mkiv.py +++ b/mkiv.py @@ -38,9 +38,10 @@ parser.add_option('--modwheel-freq-freq', dest='modffreq', type='float', help='F 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('--tempo', dest='tempo', help='Adjust interpretation of tempo (try "f1"/"global", "f2"/"track")') parser.add_option('-0', '--keep-empty', dest='keepempty', action='store_true', help='Keep (do not cull) events with 0 duration in the output file') -parser.set_defaults(tracks=[], perc='GM', deviation=2, tempo='global', modres=0.01, modfdev=1.0, modffreq=5.0, modadev=0.5, modafreq=5.0) +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) options, args = parser.parse_args() if options.tempo == 'f1': options.tempo == 'global' @@ -265,16 +266,16 @@ for fname in args: chg_prog[tidx][ev.channel] += 1 elif isinstance(ev, midi.ControlChangeEvent): if ev.control == 0: # Bank -- MSB - cur_bank[tidx][ev.channel] = (0x3F80 & cur_bank[tidx][ev.channel]) | ev.value + 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] = (0x3F & cur_bank[tidx][ev.channel]) | (ev.value << 7) + 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] = (0x3F80 & cur_mw[tidx][ev.channel]) | ev.value + 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] = (0x3F & cur_mw[tidx][ev.channel]) | (ev.value << 7) + cur_mw[tidx][ev.channel] = (0x3F80 & cur_mw[tidx][ev.channel]) | ev.value chg_mw[tidx][ev.channel] += 1 events.append(MergeEvent(ev, tidx, abstime, cur_bank[tidx][ev.channel], cur_prog[tidx][ev.channel], cur_mw[tidx][ev.channel])) ev_cnts[tidx][ev.channel] += 1 @@ -308,6 +309,9 @@ for fname in args: self.duration = dur self.modwheel = modwheel + def __repr__(self): + return ''%(MergeEvent.__repr__(self), self.pitch, self.ampl, self.duration, self.modwheel) + class NoteStream(object): __slots__ = ['history', 'active', 'bentpitch', 'modwheel'] def __init__(self): @@ -317,7 +321,7 @@ for fname in args: self.modwheel = 0 def IsActive(self): return self.active is not None - def Activate(self, mev, bentpitch = None, modwheel = None): + def Activate(self, mev, bentpitch=None, modwheel=None): if bentpitch is None: bentpitch = mev.ev.pitch self.active = mev @@ -328,6 +332,7 @@ for fname in args: 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.bentpitch = None + self.modwheel = 0 def WouldDeactivate(self, mev): if not self.IsActive(): return False @@ -474,28 +479,41 @@ for fname in args: if dev.modwheel > 0: realpitch = dev.pitch realamp = dev.ampl + mwamp = float(dev.modwheel) / 0x3FFF dt = 0.0 events = [] while dt < dev.duration: - events.append(DurationEvent(dev, realpitch + options.modfdev * math.sin(options.modffreq * (dev.abstime + dt)), realamp + options.modadev * (math.sin(options.modafreq * (dev.abstime + dt)) - 1.0) / 2.0, dev.duration, dev.modwheel)) + if options.modcont: + t = dev.abstime + dt + 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(dt, 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 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:' -- cgit v1.2.3-70-g09d2 From bab20d4625ddddad7911d548edca12cc0ea93c6b Mon Sep 17 00:00:00 2001 From: Grissess Date: Mon, 12 Sep 2016 11:52:50 -0400 Subject: DRUM SUPPORT! --- broadcast.py | 112 +++++++++++++++++++++++++++-------- client.py | 4 +- drums.py | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ drums.tar.bz2 | Bin 0 -> 3275151 bytes make_patfile.sh | 29 +++++++++ mkiv.py | 161 ++++++++++++++++++++++++++++++++++++++++++++++---- packet.py | 4 +- shiv.py | 68 +++++++++++++++++++-- 8 files changed, 515 insertions(+), 43 deletions(-) create mode 100644 drums.py create mode 100644 drums.tar.bz2 create mode 100755 make_patfile.sh (limited to 'mkiv.py') diff --git a/broadcast.py b/broadcast.py index 450874a..6747910 100644 --- a/broadcast.py +++ b/broadcast.py @@ -32,19 +32,23 @@ parser.add_option('-S', '--seek', dest='seek', type='float', help='Start time in parser.add_option('-f', '--factor', dest='factor', type='float', help='Rescale time by this factor (0 0 and dur is not None: + self.cur_offt = ttime + dur + 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.ident] = (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 @@ -487,7 +540,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 @@ -518,6 +570,7 @@ for fname in args: if options.dry: for ns in notestreams: nsq = ns.findall('note') + nsq.sort(key=lambda x: float(x.get('time'))) threads[ns] = NSThread(args=(nsq, set())) targets = threads.values() # XXX hack else: @@ -526,6 +579,7 @@ for fname in args: cli = routeset.Route(ns) if cli: 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: @@ -540,8 +594,16 @@ for fname in args: 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() + 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 + if delta >= 0 and not options.spin: + time.sleep(delta) print fname, ': Done!' diff --git a/client.py b/client.py index 8bd41a4..855fb4b 100644 --- a/client.py +++ b/client.py @@ -294,7 +294,7 @@ if options.numpy: def samps(freq, amp, phase, cnt): samps = numpy.ndarray((cnt,), numpy.int32) pvel = 2 * math.pi * freq / RATE - fac = amp / float(STREAMS) + 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) @@ -414,7 +414,7 @@ while True: 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) elif pkt.cmd == CMD.PCM: diff --git a/drums.py b/drums.py new file mode 100644 index 0000000..3f2ab9f --- /dev/null +++ b/drums.py @@ -0,0 +1,180 @@ +import pyaudio +import socket +import optparse +import tarfile +import wave +import cStringIO as StringIO +import array +import time + +from packet import Packet, CMD, stoi + +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') + +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) + +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 = set() + +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 ''%(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.discard(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.add(SampleReader(data, len(data), 1.0)) + 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) + +while True: + data = '' + while not data: + try: + data, cli = sock.recvfrom(4096) + except socket.error: + pass + pkt = Packet.FromStr(data) + print 'From', cli, 'command', pkt.cmd + if pkt.cmd == CMD.KA: + pass + elif pkt.cmd == CMD.PING: + sock.sendto(data, cli) + elif pkt.cmd == CMD.QUIT: + 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(pkt.as_float(3), 1.0), 0.0) + PLAYING.add(SampleReader(rdata, dframes * 4, amp)) + #signal.setitimer(signal.ITIMER_REAL, dur) + elif pkt.cmd == CMD.CAPS: + data = [0] * 8 + data[0] = 255 # XXX More ports? Less? + 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) +# 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 Binary files /dev/null and b/drums.tar.bz2 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 867a933..3eca1e9 100644 --- a/mkiv.py +++ b/mkiv.py @@ -39,9 +39,16 @@ parser.add_option('--modwheel-amp-dev', dest='modadev', type='float', help='Devi 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.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('--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.set_defaults(tracks=[], perc='GM', deviation=2, tempo='global', modres=0.005, modfdev=2.0, modffreq=8.0, modadev=0.5, modafreq=8.0) +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.01, stringthres=0.02, epsilon=1e-12, vol_pow=2) options, args = parser.parse_args() if options.tempo == 'f1': options.tempo == 'global' @@ -238,6 +245,8 @@ for fname in args: def __repr__(self): return ''%(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))] @@ -245,6 +254,7 @@ for fname in args: 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]) @@ -277,6 +287,14 @@ for fname in args: 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): @@ -287,11 +305,10 @@ for fname in args: 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, final modwheel, modwheel 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])) - 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...' @@ -481,13 +498,15 @@ for fname in args: 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 = dev.abstime + dt + 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(dt, dev.duration - dt), dev.modwheel)) + events.append(DurationEvent(dev, realpitch + mwamp * options.modfdev * math.sin(2 * math.pi * options.modffreq * t), realamp + mwamp * options.modadev * (math.sin(2 * math.pi * options.modafreq * t) - 1.0) / 2.0, min(options.modres, dev.duration - dt), dev.modwheel)) dt += options.modres ns.history[i:i+1] = events i += len(events) @@ -501,6 +520,95 @@ for fname in args: 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 @@ -520,6 +628,33 @@ for fname in args: for group in notegroups: print ('' 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', '' 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' @@ -557,7 +692,12 @@ for fname in args: 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) + text = tev.ev.text + # XXX Codec woes and general ET silliness + text = text.replace('\0', '') + #text = text.decode('latin_1') + #text = text.encode('ascii', 'replace') + 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') @@ -571,4 +711,5 @@ for fname in args: 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)) + txt = ET.tostring(iv, 'UTF-8') + open(os.path.splitext(os.path.basename(fname))[0]+'.iv', 'wb').write(txt) diff --git a/packet.py b/packet.py index 414ed73..1291601 100644 --- a/packet.py +++ b/packet.py @@ -27,7 +27,7 @@ class CMD: 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] diff --git a/shiv.py b/shiv.py index ac6e2b1..051d175 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,8 +24,9 @@ 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() @@ -65,6 +67,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 +88,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' if options.meta: @@ -115,10 +131,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', '') 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 +153,8 @@ for fname in args: for s in notestreams: group = s.get('group', '') 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) @@ -180,14 +207,20 @@ for fname in args: 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 + if options.total: + tot_velocities[vel] = tot_velocities.get(vel, 0) + 1 if options.vel_hist_tracks: velocities_tracks[sidx][vel] = velocities_tracks[sidx].get(vel, 0) + 1 if (options.duration or options.duty_cycle) and time + dur > max_dur: @@ -195,6 +228,9 @@ for fname in args: 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', '')) @@ -219,3 +255,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) -- cgit v1.2.3-70-g09d2 From 712ca8f06e656215c68919f9749a23bec695ccc8 Mon Sep 17 00:00:00 2001 From: Grissess Date: Fri, 11 Nov 2016 01:08:55 -0500 Subject: Rendering updates, minor bugfixes --- broadcast.py | 91 +++++++++++++++++++++++++++++++++++++++--------------------- client.py | 37 +++++++++++++++++++++--- drums.py | 10 +++++-- mkiv.py | 21 ++++++-------- 4 files changed, 108 insertions(+), 51 deletions(-) (limited to 'mkiv.py') diff --git a/broadcast.py b/broadcast.py index c8682da..2c59304 100644 --- a/broadcast.py +++ b/broadcast.py @@ -8,14 +8,15 @@ import thread import optparse import random import itertools +import re 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') @@ -48,7 +49,7 @@ parser.add_option('--pg-fullscreen', dest='fullscreen', action='store_true', hel parser.add_option('--pg-width', dest='pg_width', type='int', help='Width of the pygame window') parser.add_option('--pg-height', dest='pg_height', type='int', help='Width of the pygame window') parser.add_option('--help-routes', dest='help_routes', action='store_true', help='Show help about routing directives') -parser.set_defaults(routes=[], test_delay=0.25, random=0.0, rand_low=80, rand_high=2000, live=None, factor=1.0, duration=1.0, volume=1.0, wait_time=0.1, tries=5, play=[], transpose=0, seek=0.0, bind_addr='', ports=[13676], pg_width = 0, pg_height = 0, number=-1, pcmlead=0.1) +parser.set_defaults(routes=[], 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], pg_width = 0, pg_height = 0, number=-1, pcmlead=0.1) options, args = parser.parse_args() if options.help_routes: @@ -57,8 +58,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: @@ -68,6 +74,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...' @@ -181,15 +188,21 @@ for num in xrange(options.tries): 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.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]): @@ -219,7 +232,7 @@ if options.play: if options.test and options.sync_test: time.sleep(0.25) for cl in targets: - s.sendto(str(Packet(CMD.PLAY, 0, 250000, 880, 1.0, cl[2])), cl[:2]) + 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 @@ -330,6 +343,7 @@ if options.repeat: 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) @@ -359,7 +373,8 @@ for fname in args: BASETIME = time.time() - options.pcmlead sampcnt = 0 - buf = read_all(pcr, 16) + buf = read_all(pcr, 32) + print 'PCM: pcr', pcr, 'BASETIME', BASETIME, 'buf', len(buf) while len(buf) >= 32: frag = buf[:32] buf = buf[32:] @@ -371,7 +386,9 @@ for fname in args: if delay > 0: time.sleep(delay) if len(buf) < 32: - buf += read_all(pcr, 16) + buf += read_all(pcr, 32 - len(buf)) + print 'PCM: exit' + continue try: iv = ET.parse(fname).getroot() except IOError: @@ -390,7 +407,7 @@ for fname in args: 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': @@ -400,10 +417,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('=')) @@ -418,6 +434,8 @@ 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: @@ -432,26 +450,35 @@ for fname in args: def __init__(self, clis=None): if clis is None: clis = set(targets) - self.clients = clis + self.clients = list(clis) self.routes = [] def Route(self, stream): - testset = set(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] - if ports.get(matches[0][:2]) != OBLIGATE_POLYPHONE: - self.clients.remove(matches[0]) - return matches[0] + self.clients.remove(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: @@ -468,17 +495,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 + return [] cli = list(testset)[0] - if ports.get(cli[:2]) != OBLIGATE_POLYPHONE: - self.clients.remove(cli) + 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: @@ -513,7 +541,7 @@ for fname in args: if options.verbose: print (time.time() - BASETIME) / options.factor, ': PLAY', pitch, dur, ampl if options.dry: - playing_notes[self.ident] = (pitch, ampl) + 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]) @@ -527,7 +555,7 @@ for fname in args: print '% 6.5f'%((time.time() - BASETIME) / factor,), ': DONE' self.cur_offt = None if options.dry: - playing_notes[self.ident] = (0, 0) + playing_notes[self.nsid] = (0, 0) else: for cl in cls: playing_notes[cl] = (0, 0) @@ -560,7 +588,7 @@ for fname in args: while time.time() - BASETIME < factor*ttime: self.wait_for(factor*ttime - (time.time() - BASETIME)) if options.dry: - cl = self.ident # XXX hack + 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]) @@ -574,16 +602,17 @@ for fname in args: threads = {} if options.dry: - for ns in notestreams: + for nsid, ns in enumerate(notestreams): nsq = ns.findall('note') 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): - cli = routeset.Route(ns) - if cli: + 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: diff --git a/client.py b/client.py index 855fb4b..5c394e8 100644 --- a/client.py +++ b/client.py @@ -31,6 +31,10 @@ parser.add_option('--pg-fullscreen', dest='fullscreen', action='store_true', hel 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)') options, args = parser.parse_args() @@ -72,6 +76,7 @@ def GUI(f): def pygame_notes(): import pygame import pygame.gfxdraw + import colorsys pygame.init() dispinfo = pygame.display.Info() @@ -103,14 +108,37 @@ 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: - disp.fill((0, 0, 0), (BGR_WIDTH, 0, SAMP_WIDTH, HEIGHT)) - disp.scroll(-1, 0) - + if options.no_colback: + disp.fill((0, 0, 0), (0, 0, WIDTH, HEIGHT)) + else: + gap = WIDTH / STREAMS + for i in xrange(STREAMS): + FREQ = FREQS[i] + AMP = AMPS[i] + if FREQ > 0: + pitchval = float(FREQ - 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 * ((AMP / float(MAX)) ** 2), 1.0) + bgcol = [int(j*255) for j in bgcol] + 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] @@ -122,7 +150,7 @@ def pygame_notes(): else: pitch = 0 col = [int((AMP / MAX) * 255)] * 3 - disp.fill(col, (BGR_WIDTH - 1, HEIGHT - pitch * PFAC - PFAC, 1, PFAC)) + 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)) @@ -143,6 +171,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() diff --git a/drums.py b/drums.py index 1faca35..b912af5 100644 --- a/drums.py +++ b/drums.py @@ -18,6 +18,7 @@ parser.add_option('-u', '--uid', dest='uid', default='', help='User identifier o 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)') options, args = parser.parse_args() @@ -67,7 +68,7 @@ for fname in args: if options.verbose: print len(DRUMS), 'sounds loaded' -PLAYING = set() +PLAYING = [] class SampleReader(object): def __init__(self, buf, total, amp): @@ -108,7 +109,7 @@ def gen_data(data, frames, tm, status): for i in range(frames): fdata[i] += samps[i] for src in torem: - PLAYING.discard(src) + PLAYING.remove(src) for i in range(frames): fdata[i] = max(MIN, min(MAX, fdata[i])) fdata = array.array('i', fdata) @@ -162,7 +163,10 @@ while True: if not options.cut: dframes = rframes * ((dframes + rframes - 1) / rframes) amp = max(min(options.volume * pkt.as_float(3), 1.0), 0.0) - PLAYING.add(SampleReader(rdata, dframes * 4, amp)) + PLAYING.append(SampleReader(rdata, dframes * 4, amp)) + if options.max_voices >= 0: + while len(PLAYING) > options.max_voices: + PLAYING.pop(0) #signal.setitimer(signal.ITIMER_REAL, dur) elif pkt.cmd == CMD.CAPS: data = [0] * 8 diff --git a/mkiv.py b/mkiv.py index 3eca1e9..3ab4081 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 @@ -48,7 +44,8 @@ parser.add_option('--tempo', dest='tempo', help='Adjust interpretation of tempo 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('--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.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.01, stringthres=0.02, epsilon=1e-12, vol_pow=2) +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, vol_pow=2) options, args = parser.parse_args() if options.tempo == 'f1': options.tempo == 'global' @@ -690,14 +687,12 @@ for fname in args: ivnote.set('time', str(note.abstime)) ivnote.set('dur', str(note.duration)) - ivtext = ET.SubElement(ivstreams, 'stream', type='text') - for tev in textstream: - text = tev.ev.text - # XXX Codec woes and general ET silliness - text = text.replace('\0', '') - #text = text.decode('latin_1') - #text = text.encode('ascii', 'replace') - ivev = ET.SubElement(ivtext, 'text', time=str(tev.abstime), type=type(tev.ev).__name__, text=text) + if not options.no_text: + ivtext = ET.SubElement(ivstreams, 'stream', type='text') + for tev in textstream: + text = tev.ev.text + text = text.decode('utf8') + 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') -- cgit v1.2.3-70-g09d2 From 19d054741ba632d4cc664b89e694d07fe0b568f1 Mon Sep 17 00:00:00 2001 From: Graham Northup Date: Wed, 15 Feb 2017 19:52:01 -0500 Subject: Tools to make GRUB_INIT_TUNEs! --- downsamp.py | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ mkiv.py | 3 ++ mktune.py | 63 ++++++++++++++++++++++++++++++++ shiv.py | 8 ++--- 4 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 downsamp.py create mode 100644 mktune.py (limited to 'mkiv.py') 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/mkiv.py b/mkiv.py index 3ab4081..f363027 100644 --- a/mkiv.py +++ b/mkiv.py @@ -705,6 +705,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.' 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/shiv.py b/shiv.py index 051d175..f19ec51 100644 --- a/shiv.py +++ b/shiv.py @@ -202,7 +202,7 @@ for fname in args: notes = stream.findall('note') for note in notes: pitch = float(note.get('pitch')) - vel = int(note.get('vel')) + ampl = 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: @@ -218,11 +218,11 @@ for fname in args: 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[vel] = tot_velocities.get(vel, 0) + 1 + 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: -- cgit v1.2.3-70-g09d2 From 12f9e5ecaa7c4362d80b4aa5a5391b9132ac1cab Mon Sep 17 00:00:00 2001 From: Graham Northup Date: Tue, 21 Feb 2017 23:43:16 -0500 Subject: How was drums.py working... Also minorly improved stability of mkiv.py --- drums.py | 2 +- mkiv.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'mkiv.py') diff --git a/drums.py b/drums.py index b912af5..1ee7c1e 100644 --- a/drums.py +++ b/drums.py @@ -123,7 +123,7 @@ if options.test: print 'Current playing:', PLAYING print 'Playing:', frq data = DRUMS[frq] - PLAYING.add(SampleReader(data, len(data), 1.0)) + PLAYING.append(SampleReader(data, len(data), 1.0)) time.sleep(len(data) / (4.0 * options.rate)) print 'Done' exit() diff --git a/mkiv.py b/mkiv.py index f363027..e914f8a 100644 --- a/mkiv.py +++ b/mkiv.py @@ -691,7 +691,10 @@ for fname in args: ivtext = ET.SubElement(ivstreams, 'stream', type='text') for tev in textstream: text = tev.ev.text - text = text.decode('utf8') + 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') -- cgit v1.2.3-70-g09d2 From 7654ad67b46bb7e072cbe4a1f3dfb9c115bfeded Mon Sep 17 00:00:00 2001 From: Graham Northup Date: Mon, 12 Mar 2018 17:59:25 -0400 Subject: Slack time, 24-bit color client terminal printing, and default T:perc routing --- broadcast.py | 3 ++- client.py | 42 +++++++++++++++++++++++++++++++----------- drums.py | 34 ++++++++++++++++++++++++++++++++-- mkiv.py | 37 ++++++++++++++++++++++++++++++++++--- shiv.py | 2 +- 5 files changed, 100 insertions(+), 18 deletions(-) (limited to 'mkiv.py') diff --git a/broadcast.py b/broadcast.py index ee422c0..1efbda3 100644 --- a/broadcast.py +++ b/broadcast.py @@ -33,6 +33,7 @@ parser.add_option('-s', '--silence', dest='silence', action='store_true', help=' 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 0: - pitchval = float(FREQ - 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 * ((AMP / float(MAX)) ** 2), 1.0) - bgcol = [int(j*255) for j in bgcol] + bgcol = rgb_for_freq_amp(FREQ, float(AMP) / MAX) else: bgcol = (0, 0, 0) #print i, ':', pitchval @@ -420,6 +424,7 @@ sock.bind(('', PORT)) #signal.signal(signal.SIGALRM, sigalrm) +counter = 0 while True: data = '' while not data: @@ -428,12 +433,17 @@ 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] @@ -441,6 +451,15 @@ while True: 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 @@ -449,6 +468,7 @@ while True: 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)]) diff --git a/drums.py b/drums.py index a6d8399..62d8ae0 100644 --- a/drums.py +++ b/drums.py @@ -6,6 +6,7 @@ import wave import cStringIO as StringIO import array import time +import colorsys from packet import Packet, CMD, stoi, OBLIGATE_POLYPHONE @@ -19,6 +20,10 @@ parser.add_option('-p', '--port', dest='port', default=13676, type='int', help=' 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() @@ -31,6 +36,16 @@ if not args: parser.print_usage() exit(1) +def rgb_for_freq_amp(f, a): + 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] + DRUMS = {} for fname in args: @@ -134,6 +149,7 @@ sock.bind(('', options.port)) #signal.signal(signal.SIGALRM, sigalrm) +counter = 0 while True: data = '' while not data: @@ -142,12 +158,17 @@ 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: frq = pkt.data[2] @@ -167,6 +188,14 @@ while True: 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 @@ -175,6 +204,7 @@ while True: 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)]) diff --git a/mkiv.py b/mkiv.py index e914f8a..0c87372 100644 --- a/mkiv.py +++ b/mkiv.py @@ -42,10 +42,11 @@ parser.add_option('--string-rate-off', dest='stringoffrate', type='float', help= 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.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, vol_pow=2) +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' @@ -315,12 +316,13 @@ for fname in args: print 'Generating streams...' class DurationEvent(MergeEvent): - __slots__ = ['duration', 'pitch', 'modwheel', 'ampl'] + __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): @@ -482,6 +484,35 @@ for fname in args: print 'WARNING: Active notes at end of playback.' 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 @@ -685,7 +716,7 @@ for fname in args: 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)) + ivnote.set('dur', str(note.real_duration)) if not options.no_text: ivtext = ET.SubElement(ivstreams, 'stream', type='text') diff --git a/shiv.py b/shiv.py index fe82006..e8cc37d 100644 --- a/shiv.py +++ b/shiv.py @@ -219,7 +219,7 @@ for fname in args: notes = stream.findall('note') for note in notes: pitch = float(note.get('pitch')) - ampl = float(note.get('ampl', float(note.get('vel', 127.0)) / 127.0)) + 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: -- cgit v1.2.3-70-g09d2