|
|
|
@@ -47,24 +47,35 @@ class YSynth(object): |
|
|
|
""" |
|
|
|
|
|
|
|
# Outputs silence if no graph available |
|
|
|
if not self.graph: |
|
|
|
if self.graph is None: |
|
|
|
channels.fill(0) |
|
|
|
return |
|
|
|
|
|
|
|
# Process audio graph |
|
|
|
buf_len = channels.shape[0] |
|
|
|
self.chunk_size = buf_len |
|
|
|
try: |
|
|
|
# Process audio graph |
|
|
|
buf_len = channels.shape[0] |
|
|
|
self.chunk_size = buf_len |
|
|
|
|
|
|
|
next_chunk = next(self.graph) |
|
|
|
# Do we need to mix from mono to stereo? |
|
|
|
if len(next_chunk.shape) == 1: |
|
|
|
next_chunk = np.dot(np.reshape(next_chunk, (-1, 1)), ((1, 1),)) |
|
|
|
#print channels.shape, next_chunk.shape |
|
|
|
np.round(next_chunk * 32767.5, 0, out=channels) |
|
|
|
next_chunk = next(self.graph) |
|
|
|
# Do we need to mix from mono to stereo? |
|
|
|
if len(next_chunk.shape) == 1: |
|
|
|
next_chunk = np.dot(np.reshape(next_chunk, (-1, 1)), ((1, 1),)) |
|
|
|
#print channels.shape, next_chunk.shape |
|
|
|
np.round(next_chunk * 32767.5, 0, out=channels) |
|
|
|
|
|
|
|
# Advance sampleclock |
|
|
|
self.samples += buf_len |
|
|
|
# Advance sampleclock |
|
|
|
self.samples += buf_len |
|
|
|
|
|
|
|
# When this happens set the graph back to |
|
|
|
# None, this suppresses futher errors, and let's |
|
|
|
# a first exception slip through, causing a backtrace |
|
|
|
# in ysynth. |
|
|
|
except StopIteration: |
|
|
|
self.graph = None |
|
|
|
|
|
|
|
def set_graph(self, graph): |
|
|
|
if isinstance(graph, tuple): |
|
|
|
graph = ChannelJoiner(*graph) |
|
|
|
graph.set_synth(self) |
|
|
|
self.graph = iter(graph) |
|
|
|
|
|
|
|
@@ -110,10 +121,67 @@ class YConstant(object): |
|
|
|
|
|
|
|
def __call__(self): |
|
|
|
while True: |
|
|
|
yield self.const |
|
|
|
x = np.empty((self.synth.chunk_size,)) |
|
|
|
x.fill(self.const) |
|
|
|
yield x |
|
|
|
|
|
|
|
def set_synth(self, synth): |
|
|
|
pass |
|
|
|
self.synth = synth |
|
|
|
|
|
|
|
# XXX: Write a function that broadcasts |
|
|
|
# various channeled signals to a common |
|
|
|
# shape for operations that require this. |
|
|
|
def broadcast(stream): |
|
|
|
""" |
|
|
|
This generator takes an iterator yielding a variable number of arrays and |
|
|
|
scalars and broadcasts any arrays and scalars that require this to the |
|
|
|
correct amount of channels. If an array argument could not be normalized, |
|
|
|
this function raises a ValueError with the appropriate information. |
|
|
|
""" |
|
|
|
|
|
|
|
channels = 0 |
|
|
|
channels_shape = None |
|
|
|
require_broadcast = False |
|
|
|
stream = iter(stream) |
|
|
|
|
|
|
|
# First, check for a single multichannel count, if not |
|
|
|
# we need to raise an exception. |
|
|
|
args = next(stream) |
|
|
|
for c in args: |
|
|
|
if len(c.shape) == 2 and c.shape[1] != 1: |
|
|
|
if not channels: |
|
|
|
channels = c.shape[1] |
|
|
|
channels_shape = c.shape |
|
|
|
else: |
|
|
|
if channels != c.shape[1]: |
|
|
|
raise ValueError("Cannot broadcast input signals of" |
|
|
|
" shape %r and %r together." % (channels_shape, c.shape)) |
|
|
|
else: |
|
|
|
# There is no need for broadcasting if no differently sized |
|
|
|
# channels occur |
|
|
|
if channels: |
|
|
|
require_broadcast = True |
|
|
|
|
|
|
|
# Okay we need to broadcast every argument to 'channels' nr. of channels. |
|
|
|
if require_broadcast: |
|
|
|
while True: |
|
|
|
r = [] |
|
|
|
for c in args: |
|
|
|
if len(c.shape) != 2: |
|
|
|
c = np.reshape(c, (-1, 1)) |
|
|
|
if c.shape[1] != channels: |
|
|
|
r.append(np.dot(c, ([1] * channels,))) |
|
|
|
else: |
|
|
|
r.append(c) |
|
|
|
|
|
|
|
yield tuple(r) |
|
|
|
args = next(stream) |
|
|
|
|
|
|
|
# Or we don't need to broadcast in which case we simply pass |
|
|
|
# on our results |
|
|
|
while True: |
|
|
|
yield(args) |
|
|
|
args = next(stream) |
|
|
|
|
|
|
|
class YAudioGraphNode(object): |
|
|
|
""" |
|
|
|
@@ -127,9 +195,21 @@ class YAudioGraphNode(object): |
|
|
|
self.inputs = [] |
|
|
|
lens = 0 |
|
|
|
|
|
|
|
# Convert input arguments to audio streaming |
|
|
|
# components |
|
|
|
for stream in inputs: |
|
|
|
# Attempt to join multiple channels |
|
|
|
if isinstance(stream, tuple): |
|
|
|
if len(stream) == 1: |
|
|
|
stream = stream[1] |
|
|
|
else: |
|
|
|
stream = ChannelJoiner(*stream) |
|
|
|
|
|
|
|
# Convert constant value to audio stream |
|
|
|
if not isinstance(stream, YAudioGraphNode): |
|
|
|
stream = YConstant(stream) |
|
|
|
|
|
|
|
# Append to input signals |
|
|
|
self.inputs.append(stream) |
|
|
|
|
|
|
|
self.inputs = tuple(self.inputs) |
|
|
|
@@ -174,7 +254,7 @@ class YAudioGraphNode(object): |
|
|
|
# maybe this must be changed at a later time. |
|
|
|
|
|
|
|
# Connect the actual generator components |
|
|
|
sample_iter = iter(self(izip(*self.inputs))) |
|
|
|
sample_iter = iter(self(broadcast(izip(*self.inputs)))) |
|
|
|
|
|
|
|
# Build the sample protection function |
|
|
|
def sample_func(): |
|
|
|
@@ -230,11 +310,33 @@ class YAudioGraphNode(object): |
|
|
|
def __rdiv__(self, other): |
|
|
|
return Divisor(other, self) |
|
|
|
|
|
|
|
def __getitem__(self, delay): |
|
|
|
def __getitem__(self, key): |
|
|
|
""" |
|
|
|
Return a delayed version of the output signal. |
|
|
|
""" |
|
|
|
return Delay(self, delay) |
|
|
|
|
|
|
|
# Return delay |
|
|
|
if isinstance(key, int) or isinstance(key, float) or \ |
|
|
|
isinstance(key, YAudioGraphNode): |
|
|
|
return Delay(self, key) |
|
|
|
|
|
|
|
# Split off channels |
|
|
|
elif isinstance(key, slice): |
|
|
|
if x.start is None: |
|
|
|
start = 0 |
|
|
|
if x.stop is None: |
|
|
|
# XXX Channels here |
|
|
|
# FIXME: Incomplete code |
|
|
|
stop = self.hoi |
|
|
|
if x.step is None: |
|
|
|
step = 1 |
|
|
|
rng = xrange(start, stop, step) |
|
|
|
|
|
|
|
else: |
|
|
|
rng = key |
|
|
|
|
|
|
|
# XXX FIXME Incomplete channel split code |
|
|
|
return # Return channel ranges |
|
|
|
|
|
|
|
def __next__(self): |
|
|
|
""" |
|
|
|
@@ -299,6 +401,16 @@ class Delay(YAudioGraphNode): |
|
|
|
# Finally update sample pointer |
|
|
|
samples = (samples + self.synth.chunk_size) % buf.shape[0] |
|
|
|
|
|
|
|
# Tracker |
|
|
|
# XXX: Incomplete class |
|
|
|
class SimpleStaticTracker(YAudioGraphNode): |
|
|
|
""" |
|
|
|
Think MOD file. |
|
|
|
""" |
|
|
|
|
|
|
|
def __init__(self, track): |
|
|
|
self.track = track |
|
|
|
|
|
|
|
# Base oscillator class |
|
|
|
class YOscillator(YAudioGraphNode): |
|
|
|
def __call__(self, l): |
|
|
|
@@ -329,9 +441,11 @@ class YOscillator(YAudioGraphNode): |
|
|
|
last_cycle = [-(ifreq / float(self.synth.samplerate))] |
|
|
|
|
|
|
|
# Compute cycle using IIR filter and np.fmod |
|
|
|
# XXX: I won't work with multichannel input |
|
|
|
cycle, last_cycle = lfilter([1], [1, -1], freq / |
|
|
|
float(self.synth.samplerate) * |
|
|
|
np.ones(self.synth.chunk_size), zi=last_cycle) |
|
|
|
np.ones((self.synth.chunk_size,) + freq.shape[1:]), |
|
|
|
zi=last_cycle, axis=0) |
|
|
|
yield np.fmod(cycle, 1.0) |
|
|
|
last_cycle = np.fmod(last_cycle, 1.0) |
|
|
|
|
|
|
|
@@ -340,6 +454,17 @@ class YOscillator(YAudioGraphNode): |
|
|
|
def oscillate(self, l): |
|
|
|
raise NotImplementedError("Inherit this class") |
|
|
|
|
|
|
|
# Channel modifiers |
|
|
|
class ChannelJoiner(YAudioGraphNode): |
|
|
|
""" |
|
|
|
Join multiple input channels into a single |
|
|
|
output stream. |
|
|
|
""" |
|
|
|
|
|
|
|
def __call__(self, l): |
|
|
|
for chunks in l: |
|
|
|
yield np.hstack(np.reshape(chunk, (-1, 1)) for chunk in chunks) |
|
|
|
|
|
|
|
# Noise signals |
|
|
|
# XXX: I am probably not White Noise, fix me |
|
|
|
# up at a later time. |
|
|
|
@@ -402,6 +527,7 @@ class Pulse(YOscillator): |
|
|
|
yield np.array(pos > 0, dtype=float) |
|
|
|
|
|
|
|
# Flanger effect |
|
|
|
# FIXME: Incomplete class |
|
|
|
class Flanger(YAudioGraphNode): |
|
|
|
""" |
|
|
|
Perform virtual tape flange by mixing the |
|
|
|
|