aboutsummaryrefslogtreecommitdiff
path: root/voice.py
diff options
context:
space:
mode:
Diffstat (limited to 'voice.py')
-rw-r--r--voice.py116
1 files changed, 116 insertions, 0 deletions
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])