diff --git a/components/bl00mbox/plugins/bl00mbox/bl00mbox_line_in.c b/components/bl00mbox/plugins/bl00mbox/bl00mbox_line_in.c index a977a158cbe9fe168cf8c9295731c21fe5827bf2..2aa14d33da34adf82081f0598e5e27539f318c08 100644 --- a/components/bl00mbox/plugins/bl00mbox/bl00mbox_line_in.c +++ b/components/bl00mbox/plugins/bl00mbox/bl00mbox_line_in.c @@ -1,7 +1,7 @@ #include "bl00mbox_line_in.h" radspa_descriptor_t bl00mbox_line_in_desc = { - .name = "line_in", + .name = "bl00mbox_line_in", .id = 4001, .description = "connects to the line input of bl00mbox", .create_plugin_instance = bl00mbox_line_in_create, diff --git a/components/bl00mbox/plugins/sampler.c b/components/bl00mbox/plugins/sampler.c index e4ff33b72db5348e3c81d4f64036590ab2ba1f97..46b0a04a453b71b60f2bc9e7ce40e7c9324e1ec4 100644 --- a/components/bl00mbox/plugins/sampler.c +++ b/components/bl00mbox/plugins/sampler.c @@ -4,7 +4,9 @@ radspa_t * sampler_create(uint32_t init_var); radspa_descriptor_t sampler_desc = { .name = "_sampler_ram", .id = 696969, - .description = "simple sampler that stores a copy of the sample in ram and has basic recording functionality", + .description = "simple sampler that stores a copy of the sample in ram and has basic recording functionality." + "\ninit_var: length of pcm sample memory\ntable layout: [0:2] read head position (uint32_t), [2:4] sample start (uint32_t), " + "[4:6] sample length (uint32_t), [6] new record event (bool), [7:init_var+7] pcm sample data (int16_t)", .create_plugin_instance = sampler_create, .destroy_plugin_instance = radspa_standard_plugin_destroy }; @@ -14,6 +16,29 @@ radspa_descriptor_t sampler_desc = { #define SAMPLER_TRIGGER 1 #define SAMPLER_REC_IN 2 #define SAMPLER_REC_TRIGGER 3 +#define READ_HEAD_POS 0 +#define SAMPLE_START 2 +#define SAMPLE_LEN 4 +#define BUFFER_OFFSET 7 + +static inline int16_t define_behavior(uint32_t in){ + int32_t ret = in & 0xFFFF; + if(ret > 32767) return ret - 65535; + return ret; +} + +static inline void write_uint32_to_buffer_pos(int16_t * buf, uint32_t pos, uint32_t input){ + buf[pos] = define_behavior(input); + buf[pos+1] = define_behavior(input>>16); +} + +static inline uint32_t read_uint32_from_buffer_pos(int16_t * buf, uint32_t pos){ + int32_t lsb = buf[pos]; + if(lsb < 0) lsb += 65535; + int32_t msb = buf[pos+1]; + if(msb < 0) msb += 65535; + return (((uint32_t) msb) << 16) + ((uint32_t) lsb); +} void sampler_run(radspa_t * sampler, uint16_t num_samples, uint32_t render_pass_id){ radspa_signal_t * output_sig = radspa_signal_get_by_index(sampler, SAMPLER_OUTPUT); @@ -26,7 +51,11 @@ void sampler_run(radspa_t * sampler, uint16_t num_samples, uint32_t render_pass_ static int32_t ret = 0; - uint32_t buffer_size = sampler->plugin_table_len; + uint32_t read_head_pos = read_uint32_from_buffer_pos(buf, READ_HEAD_POS); + uint32_t sample_start = read_uint32_from_buffer_pos(buf, SAMPLE_START); + uint32_t sample_len = read_uint32_from_buffer_pos(buf, SAMPLE_LEN); + + uint32_t buffer_size = sampler->plugin_table_len - 6; for(uint16_t i = 0; i < num_samples; i++){ int16_t rec_trigger = radspa_trigger_get(rec_trigger_sig->get_value(rec_trigger_sig, i, num_samples, render_pass_id), &(data->rec_trigger_prev)); @@ -34,41 +63,52 @@ void sampler_run(radspa_t * sampler, uint16_t num_samples, uint32_t render_pass_ if(rec_trigger > 0){ if(!(data->rec_active)){ - data->read_head_position = data->sample_len; // reset sample player into off + read_head_pos = sample_len; + write_uint32_to_buffer_pos(buf, READ_HEAD_POS, read_head_pos); data->write_head_position = 0; - data->sample_len = 0; + sample_len = 0; + write_uint32_to_buffer_pos(buf, SAMPLE_LEN, sample_len); data->rec_active = true; } } else if(rec_trigger < 0){ if(data->rec_active){ - if(data->sample_len == buffer_size){ - data->sample_start = data->write_head_position; + if(sample_len == buffer_size){ + sample_start = data->write_head_position; + write_uint32_to_buffer_pos(buf, SAMPLE_START, sample_start); } else { - data->sample_start = 0; + sample_start = 0; + write_uint32_to_buffer_pos(buf, SAMPLE_START, sample_start); } + buf[6] = 1; data->rec_active = false; } } if(data->rec_active){ int16_t rec_in = rec_in_sig->get_value(rec_in_sig, i, num_samples, render_pass_id); - buf[data->write_head_position] = rec_in; + buf[data->write_head_position + BUFFER_OFFSET] = rec_in; data->write_head_position++; if(data->write_head_position >= buffer_size) data->write_head_position = 0; - if(data->sample_len < buffer_size) data->sample_len++; + if(sample_len < buffer_size){ + sample_len++; + write_uint32_to_buffer_pos(buf, SAMPLE_LEN, sample_len); + } } else { if(trigger > 0){ - data->read_head_position = 0; + read_head_pos = 0; + write_uint32_to_buffer_pos(buf, READ_HEAD_POS, read_head_pos); data->volume = trigger; } else if(trigger < 0){ - data->read_head_position = data->sample_len; + read_head_pos = sample_len; + write_uint32_to_buffer_pos(buf, READ_HEAD_POS, read_head_pos); } - if(data->read_head_position < data->sample_len){ - uint32_t sample_offset_pos = data->read_head_position + data->sample_start; - if(sample_offset_pos >= data->sample_len) sample_offset_pos -= data->sample_len; - ret = radspa_mult_shift(buf[sample_offset_pos], data->volume); - data->read_head_position++; + if(read_head_pos < sample_len){ + uint32_t sample_offset_pos = read_head_pos + sample_start; + if(sample_offset_pos >= sample_len) sample_offset_pos -= sample_len; + ret = radspa_mult_shift(buf[sample_offset_pos + BUFFER_OFFSET], data->volume); + read_head_pos++; + write_uint32_to_buffer_pos(buf, READ_HEAD_POS, read_head_pos); } else { //ret = (ret * 255)>>8; // avoid dc clicks with bad samples ret = 0; @@ -81,7 +121,7 @@ void sampler_run(radspa_t * sampler, uint16_t num_samples, uint32_t render_pass_ radspa_t * sampler_create(uint32_t init_var){ if(init_var == 0) return NULL; //doesn't make sense uint32_t buffer_size = init_var; - radspa_t * sampler = radspa_standard_plugin_create(&sampler_desc, SAMPLER_NUM_SIGNALS, sizeof(sampler_data_t), buffer_size); + radspa_t * sampler = radspa_standard_plugin_create(&sampler_desc, SAMPLER_NUM_SIGNALS, sizeof(sampler_data_t), buffer_size + BUFFER_OFFSET); if(sampler == NULL) return NULL; sampler->render = sampler_run; radspa_signal_set(sampler, SAMPLER_OUTPUT, "output", RADSPA_SIGNAL_HINT_OUTPUT, 0); @@ -92,7 +132,8 @@ radspa_t * sampler_create(uint32_t init_var){ data->trigger_prev = 0; data->rec_trigger_prev = 0; data->rec_active = false; - data->sample_start = 0; - data->sample_len = sampler->plugin_table_len; + //int16_t * buf = sampler->plugin_table; + //write_uint32_to_buffer_pos(buf, SAMPLE_START, 0); + //write_uint32_to_buffer_pos(buf, SAMPLE_LEN, sampler->plugin_table_len); return sampler; } diff --git a/components/bl00mbox/plugins/sampler.h b/components/bl00mbox/plugins/sampler.h index 9958d9b9f2a440afdaa4101b710ca032fd34d44d..8483f31cb53b19b5ec53d6e6c3517f99e2044789 100644 --- a/components/bl00mbox/plugins/sampler.h +++ b/components/bl00mbox/plugins/sampler.h @@ -3,10 +3,7 @@ #include <radspa_helpers.h> typedef struct { - uint32_t read_head_position; uint32_t write_head_position; - uint32_t sample_start; - uint32_t sample_len; int16_t trigger_prev; int16_t rec_trigger_prev; int16_t volume; diff --git a/python_payload/apps/tiny_sampler/__init__.py b/python_payload/apps/tiny_sampler/__init__.py index 7b2f38c3375a6ceb5824e5298b75f7d47326bb71..6f139be92b028454896018ce8089aeef69960e84 100644 --- a/python_payload/apps/tiny_sampler/__init__.py +++ b/python_payload/apps/tiny_sampler/__init__.py @@ -16,13 +16,13 @@ class TinySampler(Application): self.blm = bl00mbox.Channel("tiny sampler") self.samplers: List[bl00mbox.patches._Patch | Any] = [None] * 5 - self.line_in = self.blm.new(bl00mbox.plugins.line_in) + self.line_in = self.blm.new(bl00mbox.plugins.bl00mbox_line_in) self.blm.volume = ( 30000 # TODO: increase onboard mic line in gain and remove this ) self.line_in.signals.gain = 30000 for i in range(5): - self.samplers[i] = self.blm.new(bl00mbox.patches.sampler, 5000) + self.samplers[i] = self.blm.new(bl00mbox.patches.sampler, 1000) self.samplers[i].signals.output = self.blm.mixer self.samplers[i].signals.rec_in = self.line_in.signals.right self.is_recording = [False] * 5 diff --git a/python_payload/bl00mbox/_patches.py b/python_payload/bl00mbox/_patches.py index 3942399fe1d2eca9d2f7aecae40fd6778007e111..9a4f9fbbc69967c9b8350e501469ea0f132e4185 100644 --- a/python_payload/bl00mbox/_patches.py +++ b/python_payload/bl00mbox/_patches.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: CC0-1.0 import math +import os import bl00mbox import cpython.wave as wave @@ -100,61 +101,152 @@ class tinysynth_fm(tinysynth): class sampler(_Patch): """ - requires a wave file. default path: /sys/samples/ + 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._filename = "" if type(init_var) == str: filename = init_var - if filename.startswith("/flash/") or filename.startswith("/sd/"): - f = wave.open(filename, "r") - elif filename.startswith("/"): - f = wave.open("/flash/" + filename, "r") - else: - f = wave.open("/flash/sys/samples/" + filename, "r") + f = wave.open(self._convert_filename(filename), "r") - self.len_frames = f.getnframes() + self._len_frames = f.getnframes() self.plugins.sampler = chan.new( - bl00mbox.plugins._sampler_ram, self.len_frames + bl00mbox.plugins._sampler_ram, self._len_frames ) - assert f.getsampwidth() == 2 - assert f.getnchannels() in (1, 2) - assert f.getcomptype() == "NONE" - - if f.getnchannels() == 1: - # fast path for mono - table = self.plugins.sampler.table_bytearray - for i in range(0, self.len_frames * 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.len_frames): - frame = f.readframes(1) - value = int.from_bytes(frame[0:2], "little") - table[i] = value - f.close() - self._filename = filename + self.load(filename) else: - self.len_frames = int(48 * init_var) + self._len_frames = int(48 * init_var) self.plugins.sampler = chan.new( - bl00mbox.plugins._sampler_ram, self.len_frames + bl00mbox.plugins._sampler_ram, self._len_frames ) - self._filename = "" 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" + self.sample_start = 0 + 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(0, self.memory_len * 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._len_frames): + 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[_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 += 6 + return index + + @property + def memory_len(self): + return self._len_frames + + def _decode_uint32(self, pos): + lsb = self.plugins.sampler.table[pos] + msb = self.plugins.sampler.table[pos + 1] + if lsb < 0: + lsb += 65535 + if msb < 0: + msb += 65535 + return lsb + (msb * (1 << 16)) + + def _encode_uint32(self, pos, num): + msb = num // (1 << 16) + lsb = num + if lsb > 32767: + lsb -= 65535 + if msb > 32767: + msb -= 65535 + self.plugins.sampler.table[pos] = lsb + self.plugins.sampler.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): + self._encode_uint32(0, val) + + @property + def sample_start(self): + return self._decode_uint32(2) + + @sample_start.setter + def sample_start(self, val): + self._encode_uint32(2, val) + + @property + def sample_len(self): + return self._decode_uint32(4) + + @sample_len.setter + def sample_len(self, val): + self._encode_uint32(4, val) + @property def filename(self): return self._filename + @property + def rec_event_autoclear(self): + 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): @@ -190,6 +282,7 @@ class sequencer(_Patch): + str(self.signals.step_len.value) ) ret += "\n [tracks]" + """ for x, seq in enumerate(self.seqs): ret += ( "\n " @@ -198,6 +291,7 @@ class sequencer(_Patch): + "".join(["X " if x > 0 else ". " for x in seq.table[1:]]) + "]" ) + """ ret += "\n" + "\n".join(super().__repr__().split("\n")[1:]) return ret