diff options
author | Grissess <grissess@nexusg.org> | 2016-06-12 22:58:14 -0400 |
---|---|---|
committer | Grissess <grissess@nexusg.org> | 2016-06-12 22:58:14 -0400 |
commit | fd4e8f344bc7e38763871baf1f2208affa3cca59 (patch) | |
tree | fd46726bd635a2e3558531284a5da3501d57f1a8 /voice.py | |
parent | e1909c014322569a8467e3755e7313b15791ad35 (diff) | |
parent | 368b5db51d76c162656abd26c88991f0f7f8a556 (diff) |
Merge branch 'beta' into stable
Diffstat (limited to 'voice.py')
-rw-r--r-- | voice.py | 101 |
1 files changed, 101 insertions, 0 deletions
diff --git a/voice.py b/voice.py new file mode 100644 index 0000000..b31aa84 --- /dev/null +++ b/voice.py @@ -0,0 +1,101 @@ +''' +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) |