From b1e54746e876a4ed485e2ed1bbc1d8b302e908ab Mon Sep 17 00:00:00 2001 From: Grissess Date: Thu, 20 Aug 2015 23:06:11 -0400 Subject: Partial commit (sorry :( ) --- voice.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 voice.py (limited to 'voice.py') 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]) -- cgit v1.2.3-70-g09d2