-
Notifications
You must be signed in to change notification settings - Fork 1
/
voice.py
101 lines (90 loc) · 4.29 KB
/
voice.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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)