Select Git revision
mpy-cross-wrapper.c
Forked from
card10 / firmware
Source project has a limited visibility.
_patches.py 13.64 KiB
# SPDX-License-Identifier: CC0-1.0
import math
import os
import bl00mbox
import cpython.wave as wave
class _Patch:
def __init__(self, chan):
self.plugins = _PatchPluginList()
self.signals = _PatchSignalList()
self._channel = chan
def __repr__(self):
ret = "[patch] " + type(self).__name__
ret += "\n [signals:] " + "\n ".join(repr(self.signals).split("\n"))
ret += "\n [plugins:] " + "\n ".join(repr(self.plugins).split("\n"))
return ret
class _PatchItemList:
def __init__(self):
self._items = []
def __iter__(self):
return iter(self._items)
def __repr__(self):
ret = ""
for x in self._items:
a = ("\n" + repr(getattr(self, x))).split("]")
a[0] += ": " + x
ret += "]".join(a)
return ret
def __setattr__(self, key, value):
current_value = getattr(self, key, None)
if current_value is None and not key.startswith("_"):
self._items.append(key)
super().__setattr__(key, value)
class _PatchSignalList(_PatchItemList):
def __setattr__(self, key, value):
current_value = getattr(self, key, None)
if isinstance(current_value, bl00mbox.Signal):
current_value.value = value
return
super().__setattr__(key, value)
class _PatchPluginList(_PatchItemList):
pass
class tinysynth(_Patch):
def __init__(self, chan):
super().__init__(chan)
self.plugins.osc = self._channel.new(bl00mbox.plugins.osc_fm)
self.plugins.env = self._channel.new(bl00mbox.plugins.env_adsr)
self.plugins.amp = self._channel.new(bl00mbox.plugins.ampliverter)
self.plugins.amp.signals.gain = self.plugins.env.signals.output
self.plugins.amp.signals.input = self.plugins.osc.signals.output
self.plugins.env.signals.decay = 500
self.signals.output = self.plugins.amp.signals.output
self.signals.pitch = self.plugins.osc.signals.pitch
self.signals.waveform = self.plugins.osc.signals.waveform
self.signals.trigger = self.plugins.env.signals.trigger
self.signals.attack = self.plugins.env.signals.attack
self.signals.sustain = self.plugins.env.signals.sustain
self.signals.decay = self.plugins.env.signals.decay
self.signals.release = self.plugins.env.signals.release
self.signals.volume = self.plugins.env.signals.input
self.signals.release = 100
class tinysynth_fm(tinysynth):
def __init__(self, chan):
super().__init__(chan)
self.plugins.mod_osc = self._channel.new(bl00mbox.plugins.osc_fm)
self.plugins.mult = self._channel.new(bl00mbox.plugins.multipitch, 1)
self.plugins.mod_osc.signals.output = self.plugins.osc.signals.lin_fm
self.plugins.mod_osc.signals.pitch = self.plugins.mult.signals.output0
self.plugins.osc.signals.pitch = self.plugins.mult.signals.thru
self.signals.fm_waveform = self.plugins.mod_osc.signals.waveform
self.signals.fm = self.plugins.mult.signals.shift0
self.signals.pitch = self.plugins.mult.signals.input
self.signals.decay = 1000
self.signals.attack = 20
self.signals.waveform = -1
self.signals.fm_waveform = 0
self.signals.fm.tone = 3173 / 200
class sampler(_Patch):
"""
requires a wave file (str) or max sample length in milliseconds (int). default path: /sys/samples/
"""
def __init__(self, chan, init_var):
# init can be filename to load into ram
super().__init__(chan)
self.buffer_offset_i16 = 7
self._filename = ""
if type(init_var) == str:
filename = init_var
f = wave.open(self._convert_filename(filename), "r")
self._len_frames = f.getnframes()
self.plugins.sampler = chan.new(
bl00mbox.plugins._sampler_ram, self.memory_len
)
f.close()
self.load(filename)
else:
self._len_frames = int(48 * init_var)
self.plugins.sampler = chan.new(
bl00mbox.plugins._sampler_ram, self.memory_len
)
self.signals.trigger = self.plugins.sampler.signals.trigger
self.signals.output = self.plugins.sampler.signals.output
self.signals.rec_in = self.plugins.sampler.signals.rec_in
self.signals.rec_trigger = self.plugins.sampler.signals.rec_trigger
def _convert_filename(self, filename):
# TODO: waht if filename doesn't exist?
if filename.startswith("/flash/") or filename.startswith("/sd/"):
return filename
elif filename.startswith("/"):
return "/flash/" + filename
else:
return "/flash/sys/samples/" + filename
def load(self, filename):
f = wave.open(self._convert_filename(filename), "r")
assert f.getsampwidth() == 2
assert f.getnchannels() in (1, 2)
assert f.getcomptype() == "NONE"
frames = f.getnframes()
if frames > self.memory_len:
frames = self.memory_len
self.sample_len = frames
if f.getnchannels() == 1:
# fast path for mono
table = self.plugins.sampler.table_bytearray
for i in range(
2 * self.buffer_offset_i16,
(self.sample_len + self.buffer_offset_i16) * 2,
100,
):
table[i : i + 100] = f.readframes(50)
else:
# somewhat fast path for stereo
table = self.plugins.sampler.table_int16_array
for i in range(
self.buffer_offset_i16, self.sample_len + self.buffer_offset_i16
):
frame = f.readframes(1)
value = int.from_bytes(frame[0:2], "little")
table[i] = value
f.close()
def save(self, filename, overwrite=True):
# remove when we actually write
return False
if os.path.exists(filename):
if overwrite:
os.remove(filename)
else:
return False
f = wave.open(self._convert_filename(filename), "w")
for i in range(self.sample_len):
data = self.plugins.sampler.table[self._offset_index(i)]
# TODO: figure out python bytes
# note: index wraps around, ringbuffer!
# f.writeframesraw???
f.close()
return True
def _offset_index(self, index):
index += self.sample_start
if index >= self.memory_len:
index -= self.memory_len
index += self.buffer_offset_i16
return index
@property
def memory_len(self):
return self._len_frames
def _decode_uint32(self, pos):
table = self.plugins.sampler.table_int16_array
lsb = table[pos]
msb = table[pos + 1]
if lsb < 0:
lsb += 65536
if msb < 0:
msb += 65536
return lsb + (msb * (1 << 16))
def _encode_uint32(self, pos, num):
if num >= (1 << 32):
num = (1 << 32) - 1
if num < 0:
num = 0
msb = (num // (1 << 16)) & 0xFFFF
lsb = num & 0xFFFF
if lsb > 32767:
lsb -= 65536
if msb > 32767:
msb -= 65536
table = self.plugins.sampler.table_int16_array
table[pos] = lsb
table[pos + 1] = msb
@property
def read_head_position(self):
return self._decode_uint32(0)
@read_head_position.setter
def read_head_position(self, val):
if val >= self.memory_len:
val = self.memory_len - 1
self._encode_uint32(0, val)
@property
def sample_start(self):
return self._decode_uint32(2)
@sample_start.setter
def sample_start(self, val):
if val >= self.memory_len:
val = self.memory_len - 1
self._encode_uint32(2, val)
@property
def sample_len(self):
return self._decode_uint32(4)
@sample_len.setter
def sample_len(self, val):
if val > self.memory_len:
val = self.memory_len
self._encode_uint32(4, val)
@property
def filename(self):
return self._filename
@property
def rec_event_autoclear(self):
"""
returns true once after a record cycle has been completed. useful for checking whether a save is necessary if nothing else has modified the table.
"""
if self.plugins.sampler_table[6]:
self.plugins.sampler_table[6] = 0
return True
return False
class sequencer(_Patch):
def __init__(self, chan, num_tracks, num_steps):
super().__init__(chan)
self.num_steps = num_steps
self.num_tracks = num_tracks
init_var = (self.num_steps * 256) + (self.num_tracks) # magic
self.plugins.seq = chan.new(bl00mbox.plugins._sequencer, init_var)
self.signals.bpm = self.plugins.seq.signals.bpm
self.signals.beat_div = self.plugins.seq.signals.beat_div
self.signals.step = self.plugins.seq.signals.step
self.signals.step_end = self.plugins.seq.signals.step_end
self.signals.step_start = self.plugins.seq.signals.step_start
self.signals.step_start = self.plugins.seq.signals.step_start
tracktable = [-32767] + ([0] * self.num_steps)
self.plugins.seq.table = tracktable * self.num_tracks
def __repr__(self):
ret = "[patch] step sequencer"
# ret += "\n " + "\n ".join(repr(self.seqs[0]).split("\n"))
ret += (
"\n bpm: "
+ str(self.signals.bpm.value)
+ " @ 1/"
+ str(self.signals.beat_div.value)
)
ret += (
" step: "
+ str(self.signals.step.value)
+ "/"
+ str(self.signals.step_len.value)
)
ret += "\n [tracks]"
"""
for x, seq in enumerate(self.seqs):
ret += (
"\n "
+ str(x)
+ " [ "
+ "".join(["X " if x > 0 else ". " for x in seq.table[1:]])
+ "]"
)
"""
ret += "\n" + "\n".join(super().__repr__().split("\n")[1:])
return ret
def _get_table_index(self, track, step):
return step + 1 + track * (self.num_steps + 1)
def trigger_start(self, track, step, val=32767):
a = self.plugins.seq.table
a[self._get_table_index(track, step)] = val
self.plugins.seq.table = a
def trigger_stop(self, track, step, val=32767):
a = self.plugins.seq.table
a[self._get_table_index(track, step)] = -1
self.plugins.seq.table = a
def trigger_clear(self, track, step):
a = self.plugins.seq.table
a[self._get_table_index(track, step)] = 0
self.plugins.seq.table = a
def trigger_state(self, track, step):
a = self.plugins.seq.table
return a[self._get_table_index(track, step)]
def trigger_toggle(self, track, step):
if self.trigger_state(track, step) == 0:
self.trigger_start(track, step)
else:
self.trigger_clear(track, step)
class fuzz(_Patch):
def __init__(self, chan):
super().__init__(chan)
self.plugins.dist = chan.new(bl00mbox.plugins._distortion)
self.signals.input = self.plugins.dist.signals.input
self.signals.output = self.plugins.dist.signals.output
self._intensity = 2
self._volume = 32767
self._gate = 0
@property
def intensity(self):
return self._intensity
@intensity.setter
def intensity(self, val):
self._intensity = val
self._update_table()
@property
def volume(self):
return self._volume
@volume.setter
def volume(self, val):
self._volume = val
self._update_table()
@property
def gate(self):
return self._gate
@gate.setter
def gate(self, val):
self._gate = val
self._update_table()
def _update_table(self):
table = list(range(129))
for num in table:
if num < 64:
ret = num / 64 # scale to [0..1[ range
ret = ret**self._intensity
if ret > 1:
ret = 1
table[num] = int(self._volume * (ret - 1))
else:
ret = (128 - num) / 64 # scale to [0..1] range
ret = ret**self._intensity
table[num] = int(self._volume * (1 - ret))
gate = self.gate >> 9
for i in range(64 - gate, 64 + gate):
table[i] = 0
self.plugins.dist.table = table
class karplus_strong(_Patch):
def __init__(self, chan):
super().__init__(chan)
self.plugins.noise = chan._new_plugin(bl00mbox.plugins.noise_burst)
self.plugins.noise.signals.length = 25
self.plugins.flanger = chan._new_plugin(bl00mbox.plugins.flanger)
self.plugins.flanger.signals.resonance = 32500
self.plugins.flanger.signals.manual.tone = "A2"
self.plugins.flanger.signals.input = self.plugins.noise.signals.output
self.signals.trigger = self.plugins.noise.signals.trigger
self.signals.pitch = self.plugins.flanger.signals.manual
self.signals.output = self.plugins.flanger.signals.output
self.signals.level = self.plugins.flanger.signals.level
self.decay = 1000
@property
def decay(self):
return self._decay
@decay.setter
def decay(self, val):
tone = self.plugins.flanger.signals.manual.tone
loss = (50 * (2 ** (-tone / 12))) // (val / 1000)
if loss < 2:
loss = 2
self.plugins.flanger.signals.resonance = 32767 - loss
self._decay = val