diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | broadcast.py | 90 | ||||
-rw-r--r-- | client.c | 89 | ||||
-rw-r--r-- | mkiv.py | 156 |
4 files changed, 338 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66b9aca --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +client +*.iv +*.xml diff --git a/broadcast.py b/broadcast.py new file mode 100644 index 0000000..0483a67 --- /dev/null +++ b/broadcast.py @@ -0,0 +1,90 @@ +import socket +import sys +import struct +import time +import xml.etree.ElementTree as ET +import threading + +PORT = 13676 +if len(sys.argv) > 2: + factor = float(sys.argv[2]) +else: + factor = 1 + +print 'Factor:', factor + +class Packet(object): + def __init__(self, cmd, *data): + self.cmd = cmd + self.data = data + if len(data) >= 8: + raise ValueError('Too many data') + self.data = list(self.data) + [0] * (8-len(self.data)) + def __str__(self): + return struct.pack('>L'+('L'*len(self.data)), self.cmd, *self.data) + +class CMD: + KA = 0 # No important data + PING = 1 # Data are echoed exactly + QUIT = 2 # No important data + PLAY = 3 # seconds, microseconds, frequency (Hz), amplitude (0-255) + +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + +clients = [] + +s.sendto(str(Packet(CMD.PING)), ('255.255.255.255', PORT)) +s.settimeout(0.5) + +try: + while True: + data, src = s.recvfrom(4096) + clients.append(src) +except socket.timeout: + pass + +print 'Clients:' +for cl in clients: + print cl + if sys.argv[1] == '-t': + 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': + s.sendto(str(Packet(CMD.QUIT)), cl) + +try: + iv = ET.parse(sys.argv[1]).getroot() +except IOError: + print 'Bad file' + exit() + +notestreams = iv.findall("./streams/stream[@type='ns']") + +class NSThread(threading.Thread): + def run(self): + nsq, cl = self._Thread__args + for note in nsq: + ttime = float(note.get('time')) + pitch = int(note.get('pitch')) + vel = int(note.get('vel')) + dur = factor*float(note.get('dur')) + while time.time() - BASETIME < factor*ttime: + time.sleep(factor*ttime - (time.time() - BASETIME)) + s.sendto(str(Packet(CMD.PLAY, int(dur), int((dur*1000000)%1000000), int(440.0 * 2**((pitch-69)/12.0)), vel*2)), cl) + time.sleep(dur) + +threads = [] +for ns in notestreams: + if not clients: + print 'WARNING: Out of clients!' + break + nsq = ns.findall('note') + threads.append(NSThread(args=(nsq, clients.pop(0)))) + +BASETIME = time.time() +for thr in threads: + thr.start() +for thr in threads: + thr.join() diff --git a/client.c b/client.c new file mode 100644 index 0000000..31707cf --- /dev/null +++ b/client.c @@ -0,0 +1,89 @@ +#include <stdio.h> +#include <signal.h> +#include <string.h> +#include <fcntl.h> + +#include <sys/time.h> +#include <sys/socket.h> + +#include <netinet/in.h> + +#include <linux/kd.h> + +#define CLK_FREQ 1193180 + +int term; + +enum cmd_t {CMD_KA, CMD_PING, CMD_QUIT, CMD_PLAY}; + +struct cmd_buffer { + int cmd; + int data[8]; +}; + +void sigalrm(int sig) { + ioctl(term, KIOCSOUND, 0); +} + +int main(int argc, char **argv) { + struct sockaddr_in addr, remote; + int sock, rlen = sizeof(remote), i; + struct itimerval tmr; + struct cmd_buffer cmd; + struct sigaction sa; + + if((term = open("/dev/console", O_WRONLY)) < 0) { + perror("open"); + return 1; + } + if((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { + perror("socket"); + return 1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(13676); + if((bind(sock, (struct sockaddr *) &addr, sizeof(addr))) < 0) { + perror("bind"); + return 1; + } + + sa.sa_handler = sigalrm; + sa.sa_flags = SA_NODEFER | SA_RESTART; + sigemptyset(&sa.sa_mask); + sigaction(SIGALRM, &sa, NULL); + + printf("Ready to begin (listening on 13676)\n"); + memset(&tmr, 0, sizeof(tmr)); + while(1) { + if(recvfrom(sock, &cmd, sizeof(cmd), 0, (struct sockaddr *) &remote, &rlen) < 0) { + perror("recvfrom"); + return 1; + } + cmd.cmd = ntohl(cmd.cmd); + for(i = 0; i < 8; i++) cmd.data[i] = ntohl(cmd.data[i]); + /* printf("From %s:%d, cmd %d\n", inet_ntoa(remote.sin_addr.s_addr), remote.sin_port, cmd.cmd); */ + switch((enum cmd_t) cmd.cmd) { + case CMD_QUIT: + return 0; + break; + + case CMD_PING: + sendto(sock, &cmd, sizeof(cmd), 0, (struct sockaddr *) &remote, rlen); + break; + + case CMD_PLAY: + tmr.it_value.tv_sec = cmd.data[0]; + tmr.it_value.tv_usec = cmd.data[1]; + setitimer(ITIMER_REAL, &tmr, NULL); + ioctl(term, KIOCSOUND, (int) (CLK_FREQ / cmd.data[2])); + + default: + printf("WARNING: Unknown cmd %d\n", cmd.cmd); + case CMD_KA: + break; + } + } +} @@ -0,0 +1,156 @@ +''' +itl_chorus -- ITL Chorus Suite +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: +-Reserve channels by track +-Reserve channels by MIDI channel +-Pitch limits for channels +-MIDI Control events +''' + +import xml.etree.ElementTree as ET +import midi +import sys +import os + +pat = midi.read_midifile(sys.argv[1]) +iv = ET.Element('iv') +iv.set('version', '1') +iv.set('src', os.path.basename(sys.argv[1])) + +##### 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 '<ME %r in %d @%f>'%(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),) + +##### 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)) |