'''
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
import time

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 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 norm_amp(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 norm_amp(sum([i(theta) for i in self.voices]))

class VLFOMixer(Voice):
    def __init__(self, lfo, *voices):
        self.lfo = lfo
        self.voices = list(voices)
    def __call__(self, theta):
        i = int(len(self.voices) * self.lfo * (time.time() % (1.0 / self.lfo)))
        return self.voices[i](theta)
