diff options
| -rw-r--r-- | broadcast.py | 4 | ||||
| -rw-r--r-- | mkarduino.py | 29 | ||||
| -rw-r--r-- | shiv.py | 44 | ||||
| -rw-r--r-- | voice.py | 116 | 
4 files changed, 192 insertions, 1 deletions
| diff --git a/broadcast.py b/broadcast.py index 9388c06..2cff0a3 100644 --- a/broadcast.py +++ b/broadcast.py @@ -146,6 +146,8 @@ if options.live or options.list_live:          ev = S.event_input(seq.client)          event = None          if ev: +            if options.verbose: +                print 'SEQ:', ev              if ev < 0:                  seq._error(ev)              if ev.type == S.SND_SEQ_EVENT_NOTEON: @@ -163,7 +165,7 @@ if options.live or options.list_live:                  continue          if event is not None:              if isinstance(event, midi.NoteOnEvent) and event.velocity == 0: -                ev.__class__ = midi.NoteOffEvent +                event.__class__ = midi.NoteOffEvent              if options.verbose:                  print 'EVENT:', event              if isinstance(event, midi.NoteOnEvent): diff --git a/mkarduino.py b/mkarduino.py new file mode 100644 index 0000000..39926c5 --- /dev/null +++ b/mkarduino.py @@ -0,0 +1,29 @@ +#IV to arduino array computer + +import xml.etree.ElementTree as ET +import sys + +iv = ET.parse(sys.argv[1]).getroot() + +streams = iv.findall('./streams/stream[@type="ns"]') +if len(streams) > 3: +    print 'WARNING: Too many streams' + +for i in xrange(min(3, len(streams))): +    stream = streams[i] +    notes = stream.findall('note') + +# First, the header +    sys.stdout.write('const uint16_t track%d[] PROGMEM = {\n'%(i,)) + +# For the first note, write out the delay needed to get there +    if notes[0].get('time') > 0: +        sys.stdout.write('%d, 0,\n'%(int(float(notes[0].get('time'))*1000),)) + +    for idx, note in enumerate(notes): +        sys.stdout.write('%d, FREQ(%d),\n'%(int(float(note.get('dur'))*1000), int(440.0 * 2**((int(note.get('pitch'))-69)/12.0)))) +        if idx < len(notes)-1 and float(note.get('time'))+float(note.get('dur')) < float(notes[idx+1].get('time')): +            sys.stdout.write('%d, 0,\n'%(int(1000*(float(notes[idx+1].get('time')) - (float(note.get('time')) + float(note.get('dur'))))),)) + +# Finish up the stream +    sys.stdout.write('};\n\n') @@ -0,0 +1,44 @@ +# IV file viewer + +import xml.etree.ElementTree as ET +import optparse +import sys + +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('-N', '--notes', dest='notes', action='store_true', help='Show number of notes') +parser.add_option('-m', '--meta', dest='meta', action='store_true', help='Show meta track information') +parser.add_option('-h', '--histogram', dest='histogram', action='store_true', help='Show a histogram distribution of pitches') +parser.add_option('-H', '--histogram-tracks', dest='histogram_tracks', action='store_true', help='Show a histogram distribution of pitches per track') +parser.add_option('-d', '--duration', dest='duration', action='store_true', help='Show the duration of the piece') +parser.add_option('-D', '--duty-cycle', dest='duty_cycle', action='store_true', help='Show the duration of the notes within tracks, and as a percentage of the piece duration') + +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') + +options, args = parser.parse_args() + +if options.almost_all or options.all: +    options.number = True +    options.groups = True +    options.notes = True +    options.histogram = True +    options.duration = True +    if options.all: +        options.meta = True +        options.histogram_tracks= True +        options.duty_cycle = True + +for fname in args: +    try: +        iv = ET.parse(fname).getroot() +    except IOError: +        import traceback +        traceback.print_exc() +        print 'Bad file :', fname, ', skipping...' +        continue +    print +    print 'File :', fname +    print '\t<computing...>' + diff --git a/voice.py b/voice.py new file mode 100644 index 0000000..eb0b43f --- /dev/null +++ b/voice.py @@ -0,0 +1,116 @@ +''' +voice -- Voices + +A voice is a simple, singular unit of sound generation that encompasses the following +properties: +-A *generator*: some function that generates a waveform. As an input, it receives theta, + the phase of the signal it is to generate (in [0, 2pi)) and, as an output, it produces + the sample at that point, a normalized amplitude value in [-1, 1]. +-An *envelope*: a function that receives a boolean (the status of whether or not a note + is playing now) and the change in time, and outputs a factor in [0, 1] that represents + a modification to the volume of the generator (pre-output mix). +All of these functions may internally store state or other data, usually by being  +implemented as a class with a __call__ method. + +Voices are meant to generate audio data. This can be done in a number of ways, least to +most abstracted: +-A sample at a certain phase (theta) may be gotten from the generator; this can be done + by calling the voice outright; +-A set of samples can be generated via the .samples() method, which receives the number + of samples to generate and the phase velocity (a function of the sample rate and the + desired frequency of the waveform's period; this can be calculated using the static + method .phase_vel()); +-Audio data with enveloping can be generated using the .data() method, which calls the + envelope function as if the note is depressed at the given phase velocity; if the + freq is specified as None, then the note is treated as released. Note that + this will often be necessary for envelopes, as many of them are stateful (as they + depend on the first derivative of time). Also, at this level, the Voice will maintain + some state (namely, the phase at the end of generation) which will ensure (C0) smooth + transitions between already smooth generator functions, even if the frequency changes. +-Finally, a pyaudio-compatible stream callback can be provided with .pyaudio_scb(), a + method that returns a function that arranges to call .data() with the appropriate values. + The freq input to .data() will be taken from the .freq member of the voice in a possibly + non-atomic manner. +''' + +import math +import pyaudio +import struct + +def norm_theta(theta): +    return theta % (2*math.pi) + +def norm_amp(amp): +    return min(1.0, max(-1.0, amp)) + +def theta2lin(theta): +    return theta / (2*math.pi) + +def lin2theta(lin): +    return lin * 2*math.pi + +class ParamInfo(object): +    PT_ANY =       0x0000 +    PT_CONST =     0x0001 +    PT_SPECIAL =   0x0002 +    PT_INT =       0x0100 +    PT_FLOAT =     0x0200 +    PT_STR =       0x0400 +    PT_THETA =     0x0102 +    PT_TIME_SEC =  0x0202 +    PT_SAMPLES =   0x0302 +    PT_REALTIME =  0x0402 +    def __init__(self, name, tp=PT_ANY): +        self.name = name +        self.tp = tp + +class GenInfo(object): +    def __init__(self, name, *params): +        self.name = name +        self.params = list(params) + +class Generator(object): +    class __metaclass__(type): +        def __init__(self + +class Voice(object): +    @classmethod +    def register_gen(cls, name, params): +    def __init__(self, generator=None, envelope=None): +        self.generator = generator or self.DEFAULT_GENERATOR +        self.envelope = envelope or self.DEFAULT_ENVELOPE +        self.phase = 0 +        self.freq = None +    def __call__(self, theta): +        return norm_amp(self.generator(norm_theta(theta))) +    @staticmethod +    def phase_vel(freq, samp_rate): +        return 2 * math.pi * freq / samp_rate +    def samples(self, frames, pvel): +        for i in xrange(frames): +            yield self(self.phase) +            self.phase = norm_theta(self.phase + pvel) +    def data(self, frames, freq, samp_rate): +        period = 1.0/samp_rate +        status = freq is not None +        for samp in self.samples(frames, self.phase_vel(freq, samp_rate)): +            yield samp * self.envelope(status, period) +    def pyaudio_scb(self, rate, fmt=pyaudio.paInt16): +        samp_size = pyaudio.get_sample_size(fmt) +        maxint = (1 << (8*samp_size)) - 1 +        dtype = ['!', 'h', 'i', '!', 'l', '!', '!', '!', 'q'][samp_size] +        def __callback(data, frames, time, status, self=self, rate=rate, maxint=maxint, dtype=dtype): +            return struct.pack(dtype*frames, *[maxint*int(i) for i in self.data(frames, self.freq, rate)]) +        return __callback + +class VMeanMixer(Voice): +    def __init__(self, *voices): +        self.voices = list(voices) +    def __call__(self, theta): +        return sum([i(theta)/len(self.voices) for i in self.voices]) + +class VSumMixer(Voice): +    def __init__(self, *voices): +        self.voices = list(voices) +    def __call__(self, theta): +        return sum([i(theta) for i in self.voices]) | 
