Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • flow3r/flow3r-firmware
  • Vespasian/flow3r-firmware
  • alxndr42/flow3r-firmware
  • pl/flow3r-firmware
  • Kari/flow3r-firmware
  • raimue/flow3r-firmware
  • grandchild/flow3r-firmware
  • mu5tach3/flow3r-firmware
  • Nervengift/flow3r-firmware
  • arachnist/flow3r-firmware
  • TheNewCivilian/flow3r-firmware
  • alibi/flow3r-firmware
  • manuel_v/flow3r-firmware
  • xeniter/flow3r-firmware
  • maxbachmann/flow3r-firmware
  • yGifoom/flow3r-firmware
  • istobic/flow3r-firmware
  • EiNSTeiN_/flow3r-firmware
  • gnudalf/flow3r-firmware
  • 999eagle/flow3r-firmware
  • toerb/flow3r-firmware
  • pandark/flow3r-firmware
  • teal/flow3r-firmware
  • x42/flow3r-firmware
  • alufers/flow3r-firmware
  • dos/flow3r-firmware
  • yrlf/flow3r-firmware
  • LuKaRo/flow3r-firmware
  • ThomasElRubio/flow3r-firmware
  • ai/flow3r-firmware
  • T_X/flow3r-firmware
  • highTower/flow3r-firmware
  • beanieboi/flow3r-firmware
  • Woazboat/flow3r-firmware
  • gooniesbro/flow3r-firmware
  • marvino/flow3r-firmware
  • kressnerd/flow3r-firmware
  • quazgar/flow3r-firmware
  • aoid/flow3r-firmware
  • jkj/flow3r-firmware
  • naomi/flow3r-firmware
41 results
Show changes
Commits on Source (15)
Showing
with 518 additions and 107 deletions
#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,
......
......@@ -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
};
......@@ -15,6 +17,30 @@ radspa_descriptor_t sampler_desc = {
#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 - 65536;
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 += 65536;
int32_t msb = buf[pos+1];
if(msb < 0) msb += 65536;
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);
if(output_sig->buffer == NULL) return;
......@@ -26,7 +52,23 @@ 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 buffer_size = sampler->plugin_table_len - BUFFER_OFFSET;
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);
if(sample_start >= buffer_size){
sample_start = buffer_size - 1;
write_uint32_to_buffer_pos(buf, SAMPLE_START, sample_start);
}
if(sample_len >= buffer_size){
sample_len = buffer_size - 1;
write_uint32_to_buffer_pos(buf, SAMPLE_LEN, sample_len);
}
if(read_head_pos >= buffer_size){
read_head_pos = buffer_size - 1;
write_uint32_to_buffer_pos(buf, READ_HEAD_POS, read_head_pos);
}
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 +76,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 +134,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 +145,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;
}
......@@ -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;
......
set(ST3M_VERSION_PATH "${CMAKE_CURRENT_BINARY_DIR}/include/st3m_version.c")
if (NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/include/")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include/")
endif()
idf_component_register(
SRCS
st3m_audio.c
......
......@@ -43,7 +43,7 @@ bool st3m_audio_headphones_are_connected(void);
*/
bool st3m_audio_headset_is_connected(void);
/* Returns true if the lin-in jack is connected to a cable. */
/* Returns true if the line-in jack is connected to a cable. */
bool st3m_audio_line_in_is_connected(void);
/* If a sleeve contact mic doesn't pull the detection pin low enough the
......
......@@ -17,6 +17,11 @@ the "headphone" variant is chosen, else the "speaker" variant is chosen.
Returns 1 if headphones with or without microphone were connected to the
headphone jack at the last call of audio_update_jacksense.
.. py:function:: line_in_is_connected() -> bool
Returns 1 if the line-in jack was connected at the last call
of audio_update_jacksense.
.. py:function:: headphones_detection_override(enable : bool)
If a sleeve contact mic doesn't pull the detection pin low enough the
......
......@@ -3,6 +3,14 @@
``ctx`` module
==============
.. note::
Some functions might be better documented in the upstream uctx docs
https://ctx.graphics/uctx
Apologies for the divergence.
.. automodule:: ctx
:members:
:undoc-members:
......
set(SYS_C_PATH "${CMAKE_CURRENT_BINARY_DIR}/include/sys_data.c")
if (NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/include/")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include/")
endif()
idf_component_register(
SRCS
main.c
......@@ -18,11 +22,11 @@ add_custom_command(
COMMAND
"${python}" "${CMAKE_CURRENT_SOURCE_DIR}/host-tools/pack-sys.py" "${PROJECT_DIR}/python_payload" "${SYS_C_PATH}"
DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/host-tools/pack-sys.py"
"${CMAKE_CURRENT_SOURCE_DIR}/host-tools/pack-sys.py"
"${payload_files}"
)
add_custom_target(generate_sys_c ALL DEPENDS "${SYS_C_PATH}")
idf_component_get_property(main_lib main COMPONENT_LIB)
add_dependencies("${main_lib}" generate_sys_c)
\ No newline at end of file
add_dependencies("${main_lib}" generate_sys_c)
from st3m.application import Application, ApplicationContext
from st3m.input import InputState
from st3m.goose import Optional
from st3m.ui.view import ViewManager
from ctx import Context
import audio
import math
# Assume this is an enum
ForceModes = ["AUTO", "FORCE_LINE_IN", "FORCE_LINE_OUT", "FORCE_MIC", "FORCE_NONE"]
STATE_TEXT: dict[int, str] = {
audio.INPUT_SOURCE_HEADSET_MIC: "using headset mic (line out)",
audio.INPUT_SOURCE_LINE_IN: "using line in",
audio.INPUT_SOURCE_ONBOARD_MIC: "using onboard mic",
audio.INPUT_SOURCE_NONE: "plug cable to line in/out",
}
class AudioPassthrough(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
self._button_0_pressed = False
self._button_5_pressed = False
self._force_mode: str = "AUTO"
def on_enter(self, vm: Optional[ViewManager]) -> None:
self._force_mode = "AUTO"
def draw(self, ctx: Context) -> None:
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.font = ctx.get_font_name(8)
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
ctx.rgb(1, 1, 1)
# top button
ctx.move_to(105, 0)
ctx.font_size = 15
ctx.save()
ctx.rotate((math.pi / 180) * 270)
ctx.text(">")
ctx.restore()
ctx.move_to(0, -90)
ctx.text("toggle passthrough")
# middle text
ctx.font_size = 25
ctx.move_to(0, 0)
ctx.save()
if audio.input_thru_get_mute():
# 0xff4500, red
ctx.rgb(1, 0.41, 0)
else:
# 0x3cb043, green
ctx.rgb(0.24, 0.69, 0.26)
ctx.text("passthrough off" if audio.input_thru_get_mute() else "passthrough on")
ctx.restore()
# bottom text
ctx.move_to(0, 25)
ctx.save()
ctx.font_size = 15
ctx.text(STATE_TEXT.get(audio.input_get_source(), ""))
# have red text when sleep mode isn't auto
if self._force_mode != "AUTO":
# 0xff4500, red
ctx.rgb(1, 0.41, 0)
ctx.move_to(0, 40)
ctx.text("(auto)" if self._force_mode == "AUTO" else "(forced)")
# mic has a loopback risk so has precautions
# so we warn users about it to not confuse them
if self._force_mode == "FORCE_MIC":
ctx.move_to(0, 55)
ctx.text("headphones only")
ctx.move_to(0, 70)
ctx.text("will not persist app exit")
ctx.restore()
# bottom button
ctx.move_to(105, 0)
ctx.font_size = 15
ctx.save()
ctx.rotate((math.pi / 180) * 90)
ctx.text(">")
ctx.restore()
ctx.move_to(0, 90)
ctx.text("force line in/out")
def on_exit(self) -> None:
# Mic passthrough has a loopback risk
if self._force_mode == "FORCE_MIC":
self._force_mode = "FORCE_NONE"
audio.input_set_source(audio.INPUT_SOURCE_NONE)
audio.input_thru_set_mute(True)
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
headset_connected = audio.headset_is_connected()
if self._force_mode == "FORCE_MIC":
audio.input_set_source(audio.INPUT_SOURCE_ONBOARD_MIC)
elif headset_connected or self._force_mode == "FORCE_LINE_OUT":
audio.input_set_source(audio.INPUT_SOURCE_HEADSET_MIC)
elif audio.line_in_is_connected() or self._force_mode == "FORCE_LINE_IN":
audio.input_set_source(audio.INPUT_SOURCE_LINE_IN)
else:
audio.input_set_source(audio.INPUT_SOURCE_NONE)
if ins.captouch.petals[0].pressed:
if not self._button_0_pressed:
self._button_0_pressed = True
audio.input_thru_set_mute(not audio.input_thru_get_mute())
else:
self._button_0_pressed = False
if ins.captouch.petals[5].pressed:
if not self._button_5_pressed:
self._button_5_pressed = True
self._force_mode = ForceModes[ForceModes.index(self._force_mode) + 1]
if ForceModes.index(self._force_mode) >= ForceModes.index("FORCE_NONE"):
self._force_mode = "AUTO"
else:
self._button_5_pressed = False
if self._force_mode == "FORCE_MIC" and not audio.headphones_are_connected():
self._force_mode = "AUTO"
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_view(AudioPassthrough(ApplicationContext()))
[app]
name = "Audio Passthrough"
menu = "Apps"
[entry]
class = "AudioPassthrough"
[metadata]
author = "ave"
license = "LGPL-3.0-only"
url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
description = "Allows toggling audio passthrough through line-in/mic to speaker or lineout."
version = 6
from st3m.application import Application, ApplicationContext
from st3m.ui.colours import PUSH_RED, GO_GREEN, BLACK
from st3m.goose import Dict, Any, Tuple
from st3m.goose import Tuple, Any
from st3m.input import InputState
from ctx import Context
import leds
import json
import math
CONFIG_SCHEMA: dict[str, dict[str, Any]] = {
"name": {"types": [str]},
"size": {"types": [int, float], "cast_to": int},
"font": {"types": [int, float], "cast_to": int},
"pronouns": {"types": ["list_of_str"]},
"pronouns_size": {"types": [int, float], "cast_to": int},
"color": {"types": ["hex_color"]},
"mode": {"types": [int, float], "cast_to": int},
}
class Configuration:
def __init__(self) -> None:
......@@ -17,6 +26,7 @@ class Configuration:
self.pronouns_size: int = 25
self.color = "0x40ff22"
self.mode = 0
self.config_errors: list[str] = []
@classmethod
def load(cls, path: str) -> "Configuration":
......@@ -27,51 +37,59 @@ class Configuration:
data = json.loads(jsondata)
except OSError:
data = {}
if "name" in data and type(data["name"]) == str:
res.name = data["name"]
if "size" in data:
if type(data["size"]) == float:
res.size = int(data["size"])
if type(data["size"]) == int:
res.size = data["size"]
if "font" in data and type(data["font"]) == int:
res.font = data["font"]
# type checks don't look inside collections
if (
"pronouns" in data
and type(data["pronouns"]) == list
and set([type(x) for x in data["pronouns"]]) == {str}
):
res.pronouns = data["pronouns"]
if "pronouns_size" in data:
if type(data["pronouns_size"]) == float:
res.pronouns_size = int(data["pronouns_size"])
if type(data["pronouns_size"]) == int:
res.pronouns_size = data["pronouns_size"]
if (
"color" in data
and type(data["color"]) == str
and data["color"][0:2] == "0x"
and len(data["color"]) == 8
):
res.color = data["color"]
if "mode" in data:
if type(data["mode"]) == float:
res.mode = int(data["mode"])
if type(data["mode"]) == int:
res.mode = data["mode"]
except ValueError:
res.config_errors = ["nick.json decode failed!"]
data = {}
# verify the config format and generate an error message
config_type_errors: list[str] = []
for config_key, type_data in CONFIG_SCHEMA.items():
if config_key not in data.keys():
continue
key_type_valid = False
for allowed_type in type_data["types"]:
if isinstance(allowed_type, type):
if isinstance(data[config_key], allowed_type):
key_type_valid = True
break
elif allowed_type == "list_of_str":
if isinstance(data[config_key], list) and (
len(data[config_key]) == 0
or set([type(x) for x in data[config_key]]) == {str}
):
key_type_valid = True
break
elif allowed_type == "hex_color":
if (
isinstance(data[config_key], str)
and data[config_key][0:2] == "0x"
and len(data[config_key]) == 8
):
key_type_valid = True
break
if not key_type_valid:
config_type_errors.append(config_key)
else:
# Cast to relevant type if needed
if type_data.get("cast_to"):
data[config_key] = type_data["cast_to"](data[config_key])
setattr(res, config_key, data[config_key])
if config_type_errors:
res.config_errors += [
"data types wrong",
"in nick.json for:",
"",
] + config_type_errors
return res
def save(self, path: str) -> None:
d = {
"name": self.name,
"size": self.size,
"font": self.font,
"pronouns": self.pronouns,
"pronouns_size": self.pronouns_size,
"color": self.color,
"mode": self.mode,
config_key: getattr(self, config_key) for config_key in CONFIG_SCHEMA.keys()
}
jsondata = json.dumps(d)
with open(path, "w") as f:
f.write(jsondata)
......@@ -106,6 +124,19 @@ class NickApp(Application):
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
ctx.rgb(*self._config.to_normalized_tuple())
if self._config.config_errors:
draw_y = (-20 * len(self._config.config_errors)) / 2
ctx.move_to(0, draw_y)
ctx.font_size = 20
# 0xff4500, red
ctx.rgb(1, 0.41, 0)
ctx.font = ctx.get_font_name(8)
for config_error in self._config.config_errors:
draw_y += 20
ctx.move_to(0, draw_y)
ctx.text(config_error)
return
ctx.move_to(0, 0)
ctx.save()
if self._config.mode == 0:
......@@ -132,7 +163,8 @@ class NickApp(Application):
# ctx.fill()
def on_exit(self) -> None:
self._config.save(self._filename)
if not self._config.config_errors:
self._config.save(self._filename)
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
......
......@@ -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
......@@ -88,3 +88,10 @@ class TinySampler(Application):
self.is_recording[i] = False
self.ct_prev = ct
def on_exit(self) -> None:
for i in range(5):
if self.is_recording[i]:
self.samplers[i].signals.rec_trigger.stop()
self.is_recording[i] = False
audio.input_set_source(audio.INPUT_SOURCE_NONE)
# SPDX-License-Identifier: CC0-1.0
import math
import os
import bl00mbox
import cpython.wave as wave
......@@ -100,61 +101,175 @@ 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.buffer_offset_i16 = 7
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.memory_len
)
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.memory_len
)
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"
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):
......@@ -190,6 +305,7 @@ class sequencer(_Patch):
+ str(self.signals.step_len.value)
)
ret += "\n [tracks]"
"""
for x, seq in enumerate(self.seqs):
ret += (
"\n "
......@@ -198,6 +314,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
......
......@@ -10,6 +10,9 @@ def adjust_volume_dB(v: float) -> float:
def headphones_are_connected() -> bool:
pass
def headset_is_connected() -> bool:
pass
def line_in_is_connected() -> bool:
pass
......@@ -25,6 +28,12 @@ def input_set_source(source: int) -> None:
def input_get_source() -> int:
pass
def input_thru_get_mute() -> bool:
pass
def input_thru_set_mute(mute: bool) -> None:
pass
INPUT_SOURCE_NONE: int
INPUT_SOURCE_LINE_IN: int
INPUT_SOURCE_HEADSET_MIC: int
......
try:
from typing import Protocol
from typing import Protocol, Tuple
except ImportError:
from typing_extensions import Protocol # type: ignore
from typing_extensions import Protocol, Tuple # type: ignore
class Context(Protocol):
"""
......@@ -111,7 +111,8 @@ class Context(Protocol):
pass
def rotate(self, x: float) -> "Context":
"""
Add rotation to the user to device space transform.
Add rotation to the user to device space transform by specified
radians (angle * math.pi / 180).
"""
pass
def scale(self, x: float, y: float) -> "Context":
......@@ -275,13 +276,31 @@ class Context(Protocol):
TOD(q3k): document
"""
pass
def add_stop(
self, pos: float, color: Tuple[float, float, float], alpha: float
) -> "Context":
"""
Adds a color stop for a linear or radial gradient. Pos is a position between 0.0 and 1.0.
Should be called after linear_gradient or radial_gradient.
"""
pass
def linear_gradient(self, x0: float, y0: float, x1: float, y1: float) -> "Context":
"""
Change the source to a linear gradient from x0,y0 to x1,y1, by default
an empty gradient from black to white exists, add stops with
gradient_add_stop to specify a custom gradient.
add_stop to specify a custom gradient.
This is a simple example rendering a rainbow gradient on the right side of the screen:
TODO(q3k): check gradient_add_stop
>>> ctx.linear_gradient(0.18*120,0.5*120,0.95*120,0.5*120)
>>> ctx.add_stop(0.0, [1.0,0.0,0.0], 1.0)
>>> ctx.add_stop(0.2, [1.0,1.0,0.0], 1.0)
>>> ctx.add_stop(0.4, [0.0,1.0,0.0], 1.0)
>>> ctx.add_stop(0.6, [0.0,1.0,1.0], 1.0)
>>> ctx.add_stop(0.8, [0.0,0.0,1.0], 1.0)
>>> ctx.add_stop(1.0, [1.0,0.0,1.0], 1.0)
>>> ctx.rectangle(-120, -120, 240, 240)
>>> ctx.fill()
"""
pass
def radial_gradient(
......