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
Select Git revision

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
Select Git revision
Show changes
Showing
with 2125 additions and 167 deletions
with import ./pkgs.nix; with import ./pkgs.nix;
pkgs.dockerTools.buildImage { pkgs.dockerTools.buildImage {
name = "registry.k0.hswaw.net/q3k/flow3r-build"; name = "registry.gitlab.com/flow3r-badge/flow3r-build";
copyToRoot = pkgs.buildEnv { copyToRoot = pkgs.buildEnv {
name = "image-root"; name = "image-root";
paths = with pkgs; [ paths = with pkgs; [
...@@ -20,17 +20,21 @@ pkgs.dockerTools.buildImage { ...@@ -20,17 +20,21 @@ pkgs.dockerTools.buildImage {
(python3.withPackages (ps: with ps; [ (python3.withPackages (ps: with ps; [
sphinx sphinx_rtd_theme sphinx sphinx_rtd_theme
sphinx-multiversion
black black
# simulator deps # simulator deps
pygame wasmer pygame wasmer
wasmer-compiler-cranelift wasmer-compiler-cranelift
requests
])) ]))
# random build tools # random build tools
gcc gnused findutils gnugrep gcc gnused findutils gnugrep
git wget gnumake git wget gnumake
cmake ninja pkgconfig cmake ninja pkgconfig
gnutar curl bzip2
cacert
]; ];
pathsToLink = [ "/bin" ]; pathsToLink = [ "/bin" ];
}; };
...@@ -47,6 +51,7 @@ pkgs.dockerTools.buildImage { ...@@ -47,6 +51,7 @@ pkgs.dockerTools.buildImage {
"IDF_PATH=${pkgs.esp-idf}" "IDF_PATH=${pkgs.esp-idf}"
"IDF_COMPONENT_MANAGER=0" "IDF_COMPONENT_MANAGER=0"
"TMPDIR=/tmp" "TMPDIR=/tmp"
"NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
]; ];
}; };
} }
...@@ -5,4 +5,5 @@ ...@@ -5,4 +5,5 @@
esp-llvm = super.callPackage ./esp-llvm.nix {}; esp-llvm = super.callPackage ./esp-llvm.nix {};
esp-gdb = super.callPackage ./esp-gdb.nix {}; esp-gdb = super.callPackage ./esp-gdb.nix {};
run-clang-tidy = super.callPackage ./run-clang-tidy {}; run-clang-tidy = super.callPackage ./run-clang-tidy {};
mpremote = super.python310Packages.callPackage ./mpremote {};
}) })
...@@ -175,6 +175,8 @@ stdenv.mkDerivation rec { ...@@ -175,6 +175,8 @@ stdenv.mkDerivation rec {
patches = [ patches = [
./rack-off-me-nix-mate.patch ./rack-off-me-nix-mate.patch
../../../third_party/b03c8912c73fa59061d97a2f5fd5acddcc3fa356.patch ../../../third_party/b03c8912c73fa59061d97a2f5fd5acddcc3fa356.patch
../../../third_party/b6aa59f1626ef6b438eb15edf2391195a519cbfe.patch
../../../third_party/69047951dbbbef4930414eecd167ff394e1a4cc0.patch
]; ];
installPhase = '' installPhase = ''
......
{ python3
, fetchPypi
}:
python3.pkgs.buildPythonApplication rec {
pname = "mpremote";
version = "1.20.0";
format = "pyproject";
src = fetchPypi {
inherit pname version;
hash = "sha256-XDQnYqBHkTCd1JvOY8cKB1qnxUixwAdiYrlvnMw5jKI=";
};
doCheck = false;
nativeBuildInputs = with python3.pkgs; [
hatchling
hatch-requirements-txt
hatch-vcs
];
propagatedBuildInputs = with python3.pkgs; [
pyserial
importlib-metadata
];
}
...@@ -9,6 +9,25 @@ let ...@@ -9,6 +9,25 @@ let
in with nixpkgs; rec { in with nixpkgs; rec {
# nixpkgs passthrough # nixpkgs passthrough
inherit (nixpkgs) pkgs lib; inherit (nixpkgs) pkgs lib;
sphinx-multiversion =
python3Packages.buildPythonPackage rec {
pname = "sphinx-multiversion";
version = "0.2.4";
src = fetchFromGitHub {
owner = "dequis";
repo = "sphinx-multiversion";
rev = "fd3f0a0a1ef90781deac2de72c5d5c102c7f66da";
sha256 = "sha256-jW0jvLlIK1LDxPvTkeAsQnZ6JpBVQWA6IjqPKaSl8lM=";
};
doCheck = false;
propagatedBuildInputs = [
python3Packages.sphinx
];
};
# All packages require to build/lint the project. # All packages require to build/lint the project.
fwbuild = [ fwbuild = [
gcc-xtensa-esp32s3-elf-bin gcc-xtensa-esp32s3-elf-bin
...@@ -32,8 +51,11 @@ in with nixpkgs; rec { ...@@ -32,8 +51,11 @@ in with nixpkgs; rec {
python3Packages.pygame python3Packages.pygame
python3Packages.wasmer python3Packages.wasmer
python3Packages.wasmer-compiler-cranelift python3Packages.wasmer-compiler-cranelift
python3Packages.pymad
python3Packages.requests
emscripten emscripten
ncurses5 ncurses5
esp-gdb esp-gdb
mpremote
]; ];
} }
...@@ -5,7 +5,7 @@ These are the sources for the Python part of st3m, which implements the core fun ...@@ -5,7 +5,7 @@ These are the sources for the Python part of st3m, which implements the core fun
You're either seeing this in our git repository (in `python_payload`) or on a badge itself (in `/flash/sys`). You're either seeing this in our git repository (in `python_payload`) or on a badge itself (in `/flash/sys`).
On the badge, these files are required for the badge to function, and are extracted on first startup. You can edit these to your heart's conent, but this generally shouldn't be necessary. On the badge, these files are required for the badge to function, and are extracted on first startup. You can edit these to your heart's content, but this generally shouldn't be necessary.
If you break something, the badge will probably not boot. In this case, start it in recovery mode and remove the `sys` directory fully. This will cause the badge to re-extract the files on next startup. If you break something, the badge will probably not boot. In this case, start it in recovery mode and remove the `sys` directory fully. This will cause the badge to re-extract the files on next startup.
......
from st3m.application import Application
import math, random, sys_display
from st3m import settings
import leds
import sys_display
from st3m.ui import colours, led_patterns
from ctx import Context
class App(Application):
def __init__(self, app_ctx):
super().__init__(app_ctx)
self.x = 23
self.x_vel = 40 / 1000.0
self.y = 0
self.font_size = 20
self.delta_ms = 0
self.right_pressed = False
self.left_pressed = False
self.select_pressed = False
self.angle = 0
self.focused_widget = 1
self.active = False
self.num_widgets = 5
self.overhang = -30
self.line_height = 24
self.input.buttons.app.left.repeat_enable(800, 300)
self.input.buttons.app.right.repeat_enable(800, 300)
self.mid_x = 55
self.led_accumulator_ms = 0
self.blueish = False
def draw_widget(self, label):
ctx = self.ctx
self.widget_no += 1
if not self.active:
if self.select_pressed and self.focused_widget > 0:
self.active = True
self.select_pressed = False
elif self.left_pressed:
self.focused_widget -= 1
if self.focused_widget < 1:
self.focused_widget = 1
self.left_pressed = False
elif self.right_pressed:
self.focused_widget += 1
if self.focused_widget > self.num_widgets - 1:
self.focused_widget = self.num_widgets - 1
self.right_pressed = False
if self.widget_no == self.focused_widget and not self.active:
ctx.rectangle(-130, int(self.y - self.font_size * 0.8), 260, self.font_size)
ctx.line_width = 2.0
ctx.rgba(*colours.PUSH_RED, 1.0)
ctx.stroke()
ctx.gray(1)
ctx.move_to(self.mid_x, self.y)
ctx.save()
ctx.rgb(0.8, 0.8, 0.8)
ctx.text_align = ctx.RIGHT
ctx.text(label + ": ")
ctx.restore()
self.y += self.line_height
def draw_choice(self, label, choices, no):
ctx = self.ctx
self.draw_widget(label)
if self.widget_no == self.focused_widget and self.active:
if self.left_pressed:
no -= 1
if no < 0:
no = 0
elif self.right_pressed:
no += 1
if no >= len(choices):
no = len(choices) - 1
elif self.select_pressed:
self.active = False
self.select_pressed = False
for a in range(len(choices)):
if a == no and self.active and self.widget_no == self.focused_widget:
ctx.save()
ctx.rgba(*colours.PUSH_RED, 1.0)
ctx.rectangle(
ctx.x - 1,
ctx.y - self.font_size * 0.8,
ctx.text_width(choices[a]) + 2,
self.font_size,
).stroke()
ctx.restore()
ctx.text(choices[a] + " ")
elif a == no:
ctx.save()
ctx.gray(1)
ctx.rectangle(
ctx.x - 1,
ctx.y - self.font_size * 0.8,
ctx.text_width(choices[a]) + 2,
self.font_size,
).fill()
ctx.gray(0)
ctx.text(choices[a] + " ")
ctx.restore()
else:
ctx.text(choices[a] + " ")
return no
def draw_boolean(
self,
label,
value,
on_str="on",
off_str="off",
val_col=(1, 1, 1),
on_hint=None,
off_hint=None,
):
ctx = self.ctx
if ctx is None:
return
self.draw_widget(label)
if self.widget_no == self.focused_widget and self.active:
value = not value
self.active = False
ctx.save()
ctx.rgb(*val_col)
if value:
ctx.text(on_str)
else:
ctx.text(off_str)
ctx.restore()
if self.widget_no == self.focused_widget:
if value:
hint = on_hint
else:
hint = off_hint
if hint is not None:
ctx.save()
ctx.font_size -= 4
ctx.text_align = ctx.CENTER
ctx.rgb(0.9, 0.9, 0.9)
lines = hint.split("\n")
self.y -= 3
for line in lines:
ctx.move_to(0, self.y)
ctx.text(line)
self.y += self.line_height - 5
ctx.restore()
if self.y > 115:
self.focus_widget_pos_max = self.y
return value
def draw_number(self, label, step_size, no, unit=""):
ctx = self.ctx
self.draw_widget(label)
ret = no
if self.widget_no == self.focused_widget and self.active:
if self.left_pressed:
ret -= step_size
elif self.right_pressed:
ret += step_size
elif self.select_pressed:
self.active = False
self.select_pressed = False
if self.active and self.widget_no == self.focused_widget:
ctx.save()
ctx.rgba(*colours.PUSH_RED, 1.0)
ctx.rectangle(
ctx.x - 1,
ctx.y - self.font_size * 0.8,
ctx.text_width(str(no)[:4]) + 2,
self.font_size,
).stroke()
ctx.restore()
ctx.text(str(no)[:4] + unit)
else:
ctx.text(str(no)[:4] + unit)
return ret
def draw_bg(self):
ctx = self.ctx
ctx.gray(1.0)
ctx.move_to(-100, -80)
wig = self.focused_widget - 1
if wig < 2:
wig = 2
if wig > self.num_widgets - 3:
wig = self.num_widgets - 3
focus_pos = self.overhang + (wig - 0.5) * self.line_height
if focus_pos > 40:
self.overhang -= 7
if focus_pos < -40:
self.overhang += 7
self.y = self.overhang
self.widget_no = 0
ctx.rectangle(-120, -120, 240, 240)
ctx.gray(0)
ctx.fill()
ctx.save()
ctx.translate(self.x, -100)
ctx.font_size = 20
ctx.gray(0.8)
ctx.text("audio settings")
ctx.restore()
ctx.font_size = self.font_size
self.x += self.delta_ms * self.x_vel
if self.x < -50 or self.x > 50:
self.x_vel *= -1
self.x += self.delta_ms * self.x_vel
def draw(self, ctx: Context):
self.ctx = ctx
self.draw_bg()
tmp = self.draw_number(
"display brightness",
5,
int(settings.num_display_brightness.value),
unit="%",
)
if tmp < 5:
tmp = 5
if tmp > 100:
tmp = 100
if tmp != settings.num_display_brightness.value:
settings.num_display_brightness.set_value(tmp)
sys_display.set_backlight(settings.num_display_brightness.value)
tmp = self.draw_number(
"LED brightness", 5, int(settings.num_leds_brightness.value)
)
if tmp < 5:
tmp = 5
elif tmp > 255:
tmp = 255
if tmp != settings.num_leds_brightness.value:
settings.num_leds_brightness.set_value(tmp)
leds.set_brightness(settings.num_leds_brightness.value)
tmp = self.draw_number("LED speed", 5, int(settings.num_leds_speed.value))
if tmp < 0:
tmp = 0
elif tmp > 255:
tmp = 255
if tmp != settings.num_leds_speed.value:
settings.num_leds_speed.set_value(tmp)
leds.set_slew_rate(settings.num_leds_speed.value)
tmp = self.draw_boolean(
"menu LEDs",
settings.onoff_leds_random_menu.value,
on_str="rng",
off_str="user",
off_hint="set pattern with LED Painter",
)
if tmp != settings.onoff_leds_random_menu.value:
settings.onoff_leds_random_menu.set_value(tmp)
led_patterns.set_menu_colors()
leds.update()
self.delta_ms = 0
self.select_pressed = False
self.left_pressed = False
self.right_pressed = False
def think(self, ins, delta_ms):
super().think(ins, delta_ms)
self.delta_ms += delta_ms
if (
self.input.buttons.app.right.pressed
or self.input.buttons.app.right.repeated
):
self.right_pressed = True
if self.input.buttons.app.left.pressed or self.input.buttons.app.left.repeated:
self.left_pressed = True
if self.input.buttons.app.middle.pressed:
self.select_pressed = True
if self.focused_widget == 3 and leds.get_steady():
self.led_accumulator_ms += delta_ms
if self.led_accumulator_ms > 1000:
self.led_accumulator_ms = 0
led_patterns.shift_all_hsv(h=0.8)
leds.update()
def on_enter(self, vm):
super().on_enter(vm)
settings.load_all()
def on_exit(self):
settings.save_all()
super().on_exit()
if __name__ == "__main__":
from st3m.run import run_app
run_app(App)
[app]
name = "Appearance"
category = "Hidden"
[metadata]
author = "Flow3r Badge Authors"
license = "LGPL-3.0-only"
url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
from st3m.application import Application
import math, random, sys_display
from st3m import settings
import sys_audio
from st3m.ui import colours
class Drawable:
def __init__(self, press):
self.x = 23
self.x_vel = 40 / 1000.0
self.y = 0
self.font_size = 20
self.active = False
self.mid_x = 30
self.num_widgets = 2
self.overhang = -70
self.line_height = 24
self.ctx = None
self.press = press
self.focus_pos_limit_min = -60
self.focus_pos_limit_max = 60
self.focus_pos_limit_first = -60
self.focus_pos_limit_last = 80
self.first_widget_pos = 0
self.last_widget_pos = 0
self.focus_widget_pos_min = 0
self.focus_widget_pos_max = 0
self._focus_widget = 2
self._focus_widget_prev = 1
@property
def focus_widget(self):
return self._focus_widget
@property
def focus_widget_prev(self):
return self._focus_widget_prev
@focus_widget.setter
def focus_widget(self, val):
if val < 2:
val = 2
if val > self.num_widgets - 1:
val = self.num_widgets - 1
self._focus_widget_prev = self._focus_widget
self._focus_widget = val
@property
def at_first_widget(self):
return self.focus_widget <= 2
@property
def at_last_widget(self):
return self.focus_widget >= (self.num_widgets - 1)
def draw_heading(
self, label, col=(0.8, 0.8, 0.8), embiggen=6, top_margin=2, bot_margin=2
):
ctx = self.ctx
if ctx is None:
return
self.widget_no += 1
self.y += embiggen + top_margin
if self.widget_no == self.focus_widget:
if self.focus_widget > self.focus_widget_prev:
self.focus_widget += 1
else:
self.focus_widget -= 1
ctx.gray(1)
ctx.move_to(self.mid_x, self.y)
ctx.save()
ctx.rgb(*col)
ctx.move_to(0, self.y)
ctx.text_align = ctx.CENTER
ctx.font_size += embiggen
ctx.text(label)
ctx.restore()
self.y += self.line_height + embiggen + bot_margin
def draw_widget(self, label):
ctx = self.ctx
if ctx is None:
return
self.widget_no += 1
if not self.active:
if self.press.select_pressed and self.focus_widget > 0:
self.active = True
self.press.select_pressed = False
elif self.press.left_pressed:
self.focus_widget -= 1
self.press.left_pressed = False
elif self.press.right_pressed:
self.focus_widget += 1
self.press.right_pressed = False
if self.widget_no == self.focus_widget:
self.focus_widget_pos_min = self.y
if not self.active:
ctx.rectangle(
-130, int(self.y - self.font_size * 0.8), 260, self.font_size
)
ctx.line_width = 2.0
ctx.rgba(*colours.GO_GREEN, 1.0)
ctx.stroke()
self.focus_widget_pos_max = self.y + self.line_height
ctx.gray(1)
ctx.move_to(self.mid_x, self.y)
ctx.save()
ctx.rgb(0.8, 0.8, 0.8)
ctx.text_align = ctx.RIGHT
ctx.text(label + ": ")
ctx.restore()
self.y += self.line_height
def draw_choice(self, label, choices, no):
ctx = self.ctx
if ctx is None:
return
self.draw_widget(label)
if self.widget_no == self.focus_widget and self.active:
if self.press.left_pressed:
no -= 1
if no < 0:
no = 0
elif self.press.right_pressed:
no += 1
if no >= len(choices):
no = len(choices) - 1
elif self.press.select_pressed:
self.active = False
self.press.select_pressed = False
for a in range(len(choices)):
if a == no and self.active and self.widget_no == self.focus_widget:
ctx.save()
ctx.rgba(*colours.GO_GREEN, 1.0)
ctx.rectangle(
ctx.x - 1,
ctx.y - self.font_size * 0.8,
ctx.text_width(choices[a]) + 2,
self.font_size,
).stroke()
ctx.restore()
ctx.text(choices[a] + " ")
elif a == no:
ctx.save()
ctx.gray(1)
ctx.rectangle(
ctx.x - 1,
ctx.y - self.font_size * 0.8,
ctx.text_width(choices[a]) + 2,
self.font_size,
).fill()
ctx.gray(0)
ctx.text(choices[a] + " ")
ctx.restore()
else:
ctx.text(choices[a] + " ")
return no
def draw_number(self, label, step_size, no, unit="", val_col=(1.0, 1.0, 1.0)):
ctx = self.ctx
if ctx is None:
return
self.draw_widget(label)
ret = no
if self.widget_no == self.focus_widget and self.active:
if self.press.left_pressed:
ret -= step_size
elif self.press.right_pressed:
ret += step_size
elif self.press.select_pressed:
self.active = False
self.press.select_pressed = False
if self.active and self.widget_no == self.focus_widget:
ctx.save()
ctx.rgba(*colours.GO_GREEN, 1.0)
ctx.rectangle(
ctx.x - 1,
ctx.y - self.font_size * 0.8,
ctx.text_width(str(no)[:4]) + 2,
self.font_size,
).stroke()
ctx.restore()
ctx.save()
ctx.rgb(*val_col)
ctx.text(str(no)[:4] + unit)
ctx.restore()
return ret
def draw_boolean(
self,
label,
value,
on_str="on",
off_str="off",
val_col=(1.0, 1.0, 1.0),
on_hint=None,
off_hint=None,
):
ctx = self.ctx
if ctx is None:
return
self.draw_widget(label)
if self.widget_no == self.focus_widget and self.active:
value = not value
self.active = False
ctx.save()
ctx.rgb(*val_col)
if value:
ctx.text(on_str)
else:
ctx.text(off_str)
ctx.restore()
if self.widget_no == self.focus_widget:
if value:
hint = on_hint
else:
hint = off_hint
if hint is not None:
ctx.save()
ctx.font_size -= 4
ctx.text_align = ctx.CENTER
ctx.rgb(0.9, 0.9, 0.9)
lines = hint.split("\n")
self.y -= 3
for line in lines:
ctx.move_to(0, self.y)
ctx.text(line)
self.y += self.line_height - 5
ctx.restore()
if self.y > 115:
self.focus_widget_pos_max = self.y
return value
def draw_bg(self):
ctx = self.ctx
if ctx is None:
return
ctx.gray(1.0)
ctx.move_to(-100, -80)
scroll_val = 0
scroll_speed = 7
if self.at_last_widget:
if self.focus_widget_pos_max > self.focus_pos_limit_last:
scroll_val = -self.focus_widget_pos_max + self.focus_pos_limit_last
elif self.at_first_widget:
if self.focus_widget_pos_min < self.focus_pos_limit_first:
scroll_val = 9999
elif self.focus_widget_pos_max > self.focus_pos_limit_max:
scroll_val = -9999
elif self.focus_widget_pos_min < self.focus_pos_limit_min:
scroll_val = 9999
if scroll_val > 0:
self.overhang += min(scroll_val, scroll_speed)
else:
self.overhang += max(scroll_val, -scroll_speed)
self.y = self.overhang
self.widget_no = 0
ctx.rectangle(-120, -120, 240, 240)
ctx.gray(0)
ctx.fill()
ctx.font_size = self.font_size
class Submenu(Drawable):
def __init__(self, press):
super().__init__(press)
self.submenu_active = False
def draw(self, ctx):
if self.submenu_active:
self._draw(ctx)
def _draw(self, ctx):
# override w specific implementation!
pass
class SpeakerMenu(Submenu):
def __init__(self, press):
super().__init__(press)
self.num_widgets = 6
self.overhang = -40
self.mid_x = 50
self.focus_pos_limit_min = -100
self.focus_pos_limit_max = 100
self.focus_pos_limit_first = -100
self.focus_pos_limit_last = 100
def _draw(self, ctx):
self.ctx = ctx
self.draw_bg()
self.draw_heading("speaker")
tmp = self.draw_number(
"startup volume",
1,
int(settings.num_speaker_startup_volume_db.value),
unit="dB",
)
if tmp < -60:
tmp = -60
if tmp > 14:
tmp = 14
settings.num_speaker_startup_volume_db.set_value(tmp)
tmp = self.draw_number(
"minimum volume", 1, int(settings.num_speaker_min_db.value), unit="dB"
)
if settings.num_speaker_min_db.value != tmp:
sys_audio.speaker_set_minimum_volume_dB(tmp)
settings.num_speaker_min_db.set_value(
sys_audio.speaker_get_minimum_volume_dB()
)
tmp = self.draw_number(
"maximum volume", 1, int(settings.num_speaker_max_db.value), unit="dB"
)
if settings.num_speaker_max_db.value != tmp:
sys_audio.speaker_set_maximum_volume_dB(tmp)
settings.num_speaker_max_db.set_value(
sys_audio.speaker_get_maximum_volume_dB()
)
tmp = self.draw_boolean(
"equalizer", settings.onoff_speaker_eq_on.value, "soft", "off"
)
if settings.onoff_speaker_eq_on.value != tmp:
sys_audio.speaker_set_eq_on(tmp)
settings.onoff_speaker_eq_on.set_value(tmp)
class HeadphonesMenu(Submenu):
def __init__(self, press):
super().__init__(press)
self.num_widgets = 6
self.overhang = -80
self.mid_x = 50
self.focus_pos_limit_min = -20
self.focus_pos_limit_max = 100
self.focus_pos_limit_first = -40
self.focus_pos_limit_last = 100
def _draw(self, ctx):
self.ctx = ctx
self.draw_bg()
self.draw_heading("headphones")
tmp = self.draw_number(
"startup volume",
1,
int(settings.num_headphones_startup_volume_db.value),
unit="dB",
)
if tmp < -70:
tmp = -70
if tmp > 3:
tmp = 3
settings.num_headphones_startup_volume_db.set_value(tmp)
tmp = self.draw_number(
"minimum volume", 1, int(settings.num_headphones_min_db.value), unit="dB"
)
if settings.num_headphones_min_db.value != tmp:
sys_audio.headphones_set_minimum_volume_dB(tmp)
settings.num_headphones_min_db.set_value(
sys_audio.headphones_get_minimum_volume_dB()
)
tmp = self.draw_number(
"maximum volume", 1, int(settings.num_headphones_max_db.value), unit="dB"
)
if settings.num_headphones_max_db.value != tmp:
sys_audio.headphones_set_maximum_volume_dB(tmp)
settings.num_headphones_max_db.set_value(
sys_audio.headphones_get_maximum_volume_dB()
)
self.y += 8
# note: jack detection is the inverse of headphones detection override
# jack detection off means headphones detection override on
tmp = self.draw_boolean(
"jack detection",
settings.onoff_headphones_detection_override.value,
off_str="on", # sic
on_str="off", # sic
)
if settings.onoff_headphones_detection_override.value != tmp:
settings.onoff_headphones_detection_override.set_value(tmp)
sys_audio.headphones_detection_override(
settings.onoff_headphones_detection_override.value,
settings.onoff_headphones_detection_override_state.value,
)
if tmp:
self.num_widgets = 7
tmp = self.draw_boolean(
"headphone state",
settings.onoff_headphones_detection_override_state.value,
on_str="on",
off_str="off",
on_hint="sound will always play\nthrough headphones",
off_hint="sound will always play\nthrough speaker",
)
if settings.onoff_headphones_detection_override_state.value != tmp:
settings.onoff_headphones_detection_override_state.set_value(tmp)
sys_audio.headphones_detection_override(
settings.onoff_headphones_detection_override.value,
settings.onoff_headphones_detection_override_state.value,
)
else:
self.num_widgets = 6
self.overhang = -80
class VolumeControlMenu(Submenu):
def __init__(self, press):
super().__init__(press)
self.num_widgets = 6
self.overhang = -40
self.mid_x = 25
self.focus_pos_limit_min = -100
self.focus_pos_limit_max = 100
self.focus_pos_limit_first = -100
self.focus_pos_limit_last = 100
def _draw(self, ctx):
self.ctx = ctx
self.draw_bg()
self.draw_heading("volume control")
tmp = self.draw_number(
"step", 0.5, float(settings.num_volume_step_db.value), unit="dB"
)
if tmp < 0.5:
tmp = 0.5
if tmp > 5:
tmp = 5
settings.num_volume_step_db.set_value(tmp)
tmp = self.draw_number(
"repeat step",
0.5,
float(settings.num_volume_repeat_step_db.value),
unit="dB",
)
if tmp < 0:
tmp = 0
if tmp > 5:
tmp = 5
settings.num_volume_repeat_step_db.set_value(tmp)
tmp = self.draw_number(
"repeat wait",
50,
int(settings.num_volume_repeat_wait_ms.value),
unit="ms",
)
if tmp < 100:
tmp = 100
if tmp > 1000:
tmp = 1000
settings.num_volume_repeat_wait_ms.set_value(tmp)
tmp = self.draw_number(
"repeat", 50, int(settings.num_volume_repeat_ms.value), unit="ms"
)
if tmp < 100:
tmp = 100
if tmp > 1000:
tmp = 1000
settings.num_volume_repeat_ms.set_value(tmp)
class InputMenu(Submenu):
def __init__(self, press):
super().__init__(press)
self.num_widgets = 11
self.overhang = -89
self.mid_x = 0
self.focus_pos_limit_max = 80
self.focus_pos_limit_last = 100
def _draw(self, ctx):
self.ctx = ctx
self.draw_bg()
avail_col = (0.0, 0.9, 0.6)
warn_col = (0.9, 0.0, 0.0)
allow_col = (0.0, 0.7, 0.5)
not_allow_col = (0.8, 0.3, 0.3)
not_avail_col = (0.6, 0.6, 0.6)
self.draw_heading("line in", embiggen=5, top_margin=5, bot_margin=-4)
if sys_audio.line_in_get_allowed():
if sys_audio.input_engines_get_source_avail(sys_audio.INPUT_SOURCE_LINE_IN):
col = avail_col
else:
col = allow_col
else:
col = not_allow_col
tmp = self.draw_boolean(
"line in",
settings.onoff_line_in_allowed.value,
on_str="allowed",
off_str="blocked",
val_col=col,
)
if settings.onoff_line_in_allowed.value != tmp:
sys_audio.line_in_set_allowed(tmp)
settings.onoff_line_in_allowed.set_value(tmp)
tmp = self.draw_number(
"gain",
1.5,
float(settings.num_line_in_gain_db.value),
unit="dB",
)
if settings.num_line_in_gain_db.value != tmp:
sys_audio.line_in_set_gain_dB(tmp)
settings.num_line_in_gain_db.set_value(sys_audio.line_in_get_gain_dB())
self.draw_heading("headset mic", embiggen=5, top_margin=5, bot_margin=-4)
if sys_audio.headset_mic_get_allowed():
if sys_audio.input_engines_get_source_avail(
sys_audio.INPUT_SOURCE_HEADSET_MIC
):
col = avail_col
else:
col = allow_col
else:
col = not_allow_col
tmp = self.draw_boolean(
"access",
settings.onoff_headset_mic_allowed.value,
on_str="allowed",
off_str="blocked",
val_col=col,
)
if settings.onoff_headset_mic_allowed.value != tmp:
sys_audio.headset_mic_set_allowed(tmp)
settings.onoff_headset_mic_allowed.set_value(tmp)
tmp = self.draw_number(
"gain",
1.5,
float(settings.num_headset_mic_gain_db.value),
unit="dB",
)
if settings.num_headset_mic_gain_db.value != tmp:
tmp = sys_audio.headset_mic_set_gain_dB(tmp)
settings.num_headset_mic_gain_db.set_value(tmp)
self.draw_heading("onboard mic", embiggen=5, top_margin=5, bot_margin=-4)
if sys_audio.onboard_mic_get_allowed():
col = avail_col
else:
col = not_allow_col
tmp = self.draw_boolean(
"access",
settings.onoff_onboard_mic_allowed.value,
on_str="allowed",
off_str="blocked",
val_col=col,
)
if settings.onoff_onboard_mic_allowed.value != tmp:
sys_audio.onboard_mic_set_allowed(tmp)
settings.onoff_onboard_mic_allowed.set_value(tmp)
tmp = self.draw_number(
"gain",
1.5,
float(settings.num_onboard_mic_gain_db.value),
unit="dB",
)
if settings.num_onboard_mic_gain_db.value != tmp:
tmp = sys_audio.onboard_mic_set_gain_dB(tmp)
settings.num_onboard_mic_gain_db.set_value(tmp)
if not sys_audio.onboard_mic_to_speaker_get_allowed():
col = not_allow_col
else:
col = warn_col
tmp = self.draw_boolean(
"thru",
settings.onoff_onboard_mic_to_speaker_allowed.value,
on_str="allow",
off_str="phones",
val_col=col,
on_hint=" /!\ feedback possible /!\ ",
)
if settings.onoff_onboard_mic_to_speaker_allowed.value != tmp:
sys_audio.onboard_mic_to_speaker_set_allowed(tmp)
settings.onoff_onboard_mic_to_speaker_allowed.set_value(tmp)
class Press:
def __init__(self):
self.right_pressed = False
self.left_pressed = False
self.select_pressed = False
class App(Application):
def __init__(self, app_ctx):
super().__init__(app_ctx)
self.font_size = 20
# only the main app handles inputs
self.input.buttons.app.left.repeat_enable(800, 300)
self.input.buttons.app.right.repeat_enable(800, 300)
self.press = Press()
# submenus
self.menus = []
self.menus += [VolumeControlMenu(self.press)]
self.menus += [InputMenu(self.press)]
self.menus += [SpeakerMenu(self.press)]
self.menus += [HeadphonesMenu(self.press)]
def draw_bg(self):
ctx = self.ctx
if ctx is None:
return
ctx.rectangle(-120, -120, 240, 240)
ctx.gray(0)
ctx.fill()
ctx.font_size = self.font_size
ctx.gray(1)
def draw(self, ctx):
self.ctx = ctx
main_menu_active = True
for menu in self.menus:
menu.draw(self.ctx)
if menu.submenu_active:
main_menu_active = False
if main_menu_active:
self.draw_bg()
ctx.save()
ctx.rgb(*colours.GO_GREEN)
ctx.rotate(math.tau / 10)
for i in range(5):
if i == 2:
ctx.rotate(math.tau / 5)
continue
ctx.round_rectangle(-40, -110, 80, 45, 6).stroke()
ctx.rotate(math.tau / 5)
ctx.restore()
ctx.text_align = ctx.CENTER
ctx.rotate(math.tau / 10)
ctx.move_to(0, -91)
ctx.text("volume")
ctx.move_to(0, -74)
ctx.text("control")
ctx.move_to(0, 0)
ctx.rotate(math.tau * (1 / 5 + 1 / 2))
ctx.move_to(0, 92)
ctx.text("inputs")
ctx.move_to(0, 0)
ctx.rotate(math.tau * 2 / 5)
ctx.move_to(0, 92)
ctx.text("speaker")
ctx.move_to(0, 0)
ctx.rotate(math.tau * (1 / 5 + 1 / 2))
ctx.move_to(0, -91)
ctx.text("head")
ctx.move_to(0, -74)
ctx.text("phones")
ctx.rotate(math.tau / 10)
ctx.move_to(0, 0)
ctx.text("audio config")
ctx.rgb(0.7, 0.7, 0.7)
ctx.font_size = 18
ctx.move_to(0, 20)
ctx.text("exit to save")
self.press.select_pressed = False
self.press.left_pressed = False
self.press.right_pressed = False
def think(self, ins, delta_ms):
super().think(ins, delta_ms)
for i in range(1, 10, 2):
if self.input.captouch.petals[i].whole.pressed:
for menu in self.menus:
menu.submenu_active = False
if i < 5:
self.menus[i // 2].submenu_active = True
elif i > 5:
self.menus[(i // 2) - 1].submenu_active = True
if (
self.input.buttons.app.right.pressed
or self.input.buttons.app.right.repeated
):
self.press.right_pressed = True
if self.input.buttons.app.left.pressed or self.input.buttons.app.left.repeated:
self.press.left_pressed = True
if self.input.buttons.app.middle.pressed:
self.press.select_pressed = True
def on_enter(self, vm):
super().on_enter(vm)
settings.load_all()
def on_exit(self):
settings.save_all()
super().on_exit()
if __name__ == "__main__":
from st3m.run import run_app
run_app(App)
[app]
name = "Audio Config"
category = "Hidden"
[metadata]
author = "Flow3r Badge Authors"
license = "LGPL-3.0-only"
url = "https://git.flow3r.garden/flow3r/flow3r-firmware"
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"]
STATE_TEXT: dict[int, str] = {
audio.INPUT_SOURCE_AUTO: "auto",
audio.INPUT_SOURCE_HEADSET_MIC: "headset mic",
audio.INPUT_SOURCE_LINE_IN: "line in",
audio.INPUT_SOURCE_ONBOARD_MIC: "onboard mic",
}
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"
self._mute = True
self._source = None
self.target_source = audio.INPUT_SOURCE_AUTO
def on_enter(self, vm: Optional[ViewManager]) -> None:
super().on_enter(vm)
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 self._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 self._mute else "passthrough on")
ctx.restore()
# bottom text
ctx.move_to(0, 25)
ctx.save()
ctx.font_size = 15
ctx.text(STATE_TEXT.get(self.target_source, ""))
ctx.move_to(0, 40)
if self.source_connected:
# 0x3cb043, green
ctx.rgb(0.24, 0.69, 0.26)
else:
# 0xff4500, red
ctx.rgb(1, 0.41, 0)
if self._mute:
ctx.text("standby")
elif self._force_mode == "AUTO":
src = audio.input_thru_get_source()
if src != audio.INPUT_SOURCE_NONE:
ctx.text("connected to")
ctx.move_to(0, 56)
ctx.text(STATE_TEXT.get(src, ""))
else:
ctx.text("waiting...")
elif self._force_mode == "FORCE_MIC":
ctx.text("connected" if self.source_connected else "(headphones only)")
else:
ctx.text("connected" if self.source_connected else "waiting...")
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("next source")
@property
def source_connected(self):
if self.source != audio.INPUT_SOURCE_NONE:
return self.source == audio.input_thru_get_source()
else:
return False
@property
def source(self):
if self._source is None:
self._source = audio.input_thru_get_source()
return self._source
@source.setter
def source(self, source):
audio.input_thru_set_source(source)
self._source = audio.input_thru_get_source()
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
if ins.captouch.petals[0].pressed:
if not self._button_0_pressed:
self._button_0_pressed = True
self._mute = not self._mute
else:
self._button_0_pressed = False
if ins.captouch.petals[5].pressed:
if not self._button_5_pressed:
self._button_5_pressed = True
index = ForceModes.index(self._force_mode)
index = (index + 1) % 4
self._force_mode = ForceModes[index]
else:
self._button_5_pressed = False
if self._mute:
self.source = audio.INPUT_SOURCE_NONE
else:
if self._force_mode == "FORCE_MIC":
self.target_source = audio.INPUT_SOURCE_ONBOARD_MIC
elif self._force_mode == "AUTO":
self.target_source = audio.INPUT_SOURCE_AUTO
elif self._force_mode == "FORCE_LINE_IN":
self.target_source = audio.INPUT_SOURCE_LINE_IN
elif self._force_mode == "FORCE_LINE_OUT":
self.target_source = audio.INPUT_SOURCE_HEADSET_MIC
self.source = self.target_source
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(AudioPassthrough)
[app]
name = "Audio Passthrough"
category = "Media"
[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 = 7
import st3m.run, random import random
from st3m.application import Application
from st3m.application import Application, ApplicationContext import sys_display
from st3m.input import InputState
from ctx import Context
class Cloud: class Cloud:
def __init__(self, x: float, y: float, z: float) -> None: def __init__(self, path, x, y, z):
self.path = path
self.x = x self.x = x
self.y = y self.y = y
self.z = z self.z = z
def draw(self, ctx: Context) -> None: def draw(self, ctx) -> None:
x = self.x / self.z * 120 x = self.x / self.z * 120
y = self.y / self.z * 120 y = self.y / self.z * 120
width = 200.0 / self.z * 120 width = 200.0 / self.z * 160
height = 100.0 / self.z * 120 height = 100.0 / self.z * 160
ctx.image( ctx.image(
"/flash/sys/apps/clouds/cloud.png", self.path,
x - width / 2, x - width / 2,
y - height / 2, y - height / 2,
width, width,
...@@ -25,28 +24,45 @@ class Cloud: ...@@ -25,28 +24,45 @@ class Cloud:
) )
class Clouds(Application): class App(Application):
def __init__(self, app_ctx: ApplicationContext) -> None: def __init__(self, app_ctx):
super().__init__(app_ctx) super().__init__(app_ctx)
self.clouds = [] self.clouds = []
for i in range(10): for i in range(10):
self.clouds.append( self.clouds.append(
Cloud( Cloud(
app_ctx.bundle_path + "/cloud.png",
((random.getrandbits(16) - 32767) / 32767.0) * 200, ((random.getrandbits(16) - 32767) / 32767.0) * 200,
((random.getrandbits(16)) / 65535.0) * 50 - 5, ((random.getrandbits(16)) / 65535.0) * 60 - 10,
((random.getrandbits(16)) / 65535.0) * 200 + 5, ((random.getrandbits(16)) / 65535.0) * 200 + 5 + i / 10.0,
) )
) )
def think(self, ins: InputState, delta_ms: int) -> None: def think(self, ins, delta_ms):
super().think(ins, delta_ms) super().think(ins, delta_ms)
no = 0
for c in self.clouds: for c in self.clouds:
c.z -= 40 * delta_ms / 1000.0 c.x -= (delta_ms / 1000.0) * ins.imu.acc[1] * 10
c.z -= (delta_ms / 1000.0) * (ins.imu.acc[2] - 5) * 20
# wrap x and z coordinates around
if c.z < 10: if c.z < 10:
c.z = 300 c.z = 300
c.z = 300.0 + no / 10.0
elif c.z > 300:
c.z = 10
c.z = 10.0 + no / 10.0
if c.x < -200:
c.x = 200
c.x = 200.0
elif c.x > 200:
c.x = -200
c.x = -200.0
no = no + 1
self.clouds = sorted(self.clouds, key=lambda c: -c.z) self.clouds = sorted(self.clouds, key=lambda c: -c.z)
def draw(self, ctx: Context) -> None: def draw(self, ctx):
# faster, and with smoothing is incorrect
ctx.image_smoothing = False ctx.image_smoothing = False
ctx.rectangle(-120, -120, 240, 120) ctx.rectangle(-120, -120, 240, 120)
ctx.rgb(0, 0.34, 0.72) ctx.rgb(0, 0.34, 0.72)
...@@ -58,6 +74,12 @@ class Clouds(Application): ...@@ -58,6 +74,12 @@ class Clouds(Application):
for c in self.clouds: for c in self.clouds:
c.draw(ctx) c.draw(ctx)
def on_enter(self, vm):
super().on_enter(vm)
sys_display.set_mode(24 + sys_display.osd)
if __name__ == "__main__":
from st3m.run import run_app
# uncomment to make runnable via mpremote run_app(App, "/flash/sys/apps/clouds")
# st3m.run.run_view(Clouds(ApplicationContext()))
[app] [app]
name = "Clouds" name = "Clouds"
menu = "Apps" category = "Badge"
[entry]
class = "Clouds"
[metadata] [metadata]
author = "Flow3r Badge Authors" author = "Flow3r Badge Authors"
......
from .main import CapTouchDemo from st3m.application import Application
from st3m.goose import List
from st3m.input import InputState
from st3m.ui.view import ViewManager
from ctx import Context
App = CapTouchDemo import captouch
import cmath
import math
class Dot:
size = None
pos = 0j
filled = True
def draw(self, ctx: Context):
if self.size is None or self.pos is None or self.filled is None:
return
ctx.save()
ctx.translate(self.pos.real, self.pos.imag)
ctx.rotate(cmath.phase(self.pos))
ctx.move_to(-self.size / 2, -self.size / 2)
ctx.rel_line_to(self.size, self.size / 2)
ctx.rel_line_to(-self.size, self.size / 2)
ctx.close_path()
ctx.fill() if self.filled else ctx.stroke()
ctx.restore()
class CapTouchDemo(Application):
def on_enter(self, vm: ViewManager):
super().on_enter(vm)
self.dots: List[Dot] = [Dot() for x in range(10)]
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
for i in range(10):
petal = ins.captouch.petals[i]
dot = self.dots[i]
pos = 0j if petal.pos is None else petal.pos * 30
dot.pos = (pos + 70) * captouch.PETAL_ROTORS[i]
dot.size = 5 + 8 * math.sqrt(petal.raw_cap)
dot.filled = not petal.pressed
def draw(self, ctx: Context) -> None:
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
for i in range(10):
ctx.rgb(*((0.0, 0.8, 0.8) if i % 2 else (1.0, 0.0, 1.0)))
self.dots[i].draw(ctx)
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(CapTouchDemo)
[app] [app]
name = "Captouch Demo" name = "captouch demo"
menu = "Apps" category = "Demos"
[entry] [entry]
class = "App" class = "CapTouchDemo"
[metadata] [metadata]
author = "Flow3r Badge Authors" author = "Flow3r Badge Authors"
......
from st3m import logging
from st3m.application import Application, ApplicationContext
from st3m.goose import List
from st3m.input import InputState
from ctx import Context
log = logging.Log(__name__, level=logging.INFO)
log.info("import")
import cmath
import math
import time
class Dot:
def __init__(self, size: float, imag: float, real: float) -> None:
self.size = size
self.imag = imag
self.real = real
def draw(self, i: int, ctx: Context) -> None:
imag = self.imag
real = self.real
size = self.size
col = (1.0, 0.0, 1.0)
if i % 2:
col = (0.0, 1.0, 1.0)
ctx.rgb(*col).rectangle(
-int(imag - (size / 2)), -int(real - (size / 2)), size, size
).fill()
class CapTouchDemo(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
self.dots: List[Dot] = []
self.last_calib = None
# self.ui_autocalib = ui.IconLabel("Autocalib done", size=30)
# self.add_event(
# event.Event(
# name="captouch_autocalib",
# action=self.do_autocalib,
# condition=lambda data: data["type"] == "button"
# and data["index"] == 0
# and data["change"]
# and data["from"] == 2,
# enabled=True,
# )
# )
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
self.dots = []
for i in range(10):
petal = ins.captouch.petals[i]
(rad, phi) = petal.position
size = 4
if petal.pressed:
size += 4
x = 70 + (rad / 1000) + 0j
x += ((-phi) / 600) * 1j
rot = cmath.exp(-2j * math.pi * i / 10)
x = x * rot
self.dots.append(Dot(size, x.imag, x.real))
def draw(self, ctx: Context) -> None:
# print(self.last_calib)
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
for i, dot in enumerate(self.dots):
dot.draw(i, ctx)
# if not self.last_calib is None and self.last_calib > 0:
# self.last_calib -= 1
# self.ui_autocalib.draw(ctx)
# def do_autocalib(self, data) -> None:
# log.info("Performing captouch autocalibration")
# captouch.calibration_request()
# self.last_calib = 50
import captouch import captouch
import bl00mbox import bl00mbox
blm = bl00mbox.Channel("Harmonic Demo")
import leds import leds
import errno
from st3m.goose import List from st3m.goose import List
from st3m.input import InputState from st3m.input import InputState
from st3m.goose import Optional
from st3m.ui.view import ViewManager
from ctx import Context from ctx import Context
import cmath
import math
import json
import random
chords = [ from st3m.ui import colours
[-4, 0, 3, 8, 10],
[-3, 0, 5, 7, 12], tai = math.tau * 1j
[-1, 2, 5, 7, 11],
[0, 3, 7, 12, 14],
[3, 7, 10, 14, 15],
]
from st3m.application import Application, ApplicationContext from st3m.application import Application, ApplicationContext
class chord_organ_synth(bl00mbox.patches._Patch):
def __init__(self, chan):
super().__init__(chan)
self.plugins.osc = self._channel.new(bl00mbox.plugins.osc)
self.plugins.env = self._channel.new(bl00mbox.plugins.env_adsr)
self.plugins.env.signals.decay = 500
self.plugins.env.signals.release = 800
self.plugins.env.signals.attack = 100
self.plugins.env.signals.sustain = 25979
self.plugins.env.signals.input = self.plugins.osc.signals.output
self.plugins.osc_harm1 = self._channel.new(bl00mbox.plugins.osc)
self.plugins.env_harm1 = self._channel.new(bl00mbox.plugins.env_adsr)
self.plugins.env_harm1.signals.decay = 500
self.plugins.env_harm1.signals.release = 1200
self.plugins.env_harm1.signals.attack = 20
self.plugins.env_harm1.signals.sustain = 25979
self.plugins.env_harm1.signals.gain.dB = -9
self.plugins.env_harm1.signals.input = self.plugins.osc_harm1.signals.output
self.plugins.osc_harm2 = self._channel.new(bl00mbox.plugins.osc)
self.plugins.env_harm2 = self._channel.new(bl00mbox.plugins.env_adsr)
self.plugins.env_harm2.signals.decay = 500
self.plugins.env_harm2.signals.release = 1400
self.plugins.env_harm2.signals.attack = 300
self.plugins.env_harm2.signals.sustain = 25979
self.plugins.env_harm2.signals.gain.dB = -18
self.plugins.env_harm2.signals.input = self.plugins.osc_harm2.signals.output
self.plugins.mixer = self._channel.new(bl00mbox.plugins.mixer, 3)
self.plugins.mixer.signals.gain.dB -= 3
# self.plugins.mixer.signals.block_dc.switch.ON = True
for i, x in enumerate(
[self.plugins.env, self.plugins.env_harm1, self.plugins.env_harm2]
):
self.plugins.mixer.signals.input[i] = x.signals.output
self.plugins.multipitch = self._channel.new(bl00mbox.plugins.multipitch, 2)
self.plugins.multipitch.signals.thru = self.plugins.osc.signals.pitch
self.plugins.multipitch.signals.output[0] = self.plugins.osc_harm1.signals.pitch
self.plugins.multipitch.signals.output[1] = self.plugins.osc_harm2.signals.pitch
self.plugins.multipitch.signals.trigger_thru = self.plugins.env.signals.trigger
self.plugins.multipitch.signals.trigger_thru = (
self.plugins.env_harm1.signals.trigger
)
self.plugins.multipitch.signals.trigger_thru = (
self.plugins.env_harm2.signals.trigger
)
self.plugins.multipitch.signals.shift[0].tone = 12 + 7
self.plugins.multipitch.signals.shift[1].tone = 24 + 4
self.signals.pitch = self.plugins.multipitch.signals.input
self.signals.output = self.plugins.mixer.signals.output
self.signals.trigger = self.plugins.multipitch.signals.trigger_in
def tone_to_note_name(tone):
# TODO: add this to radspa helpers
sct = tone * 200 + 18367
return bl00mbox.helpers.sct_to_note_name(sct)
def note_name_to_tone(note_name):
# TODO: add this to radspa helpers
sct = bl00mbox.helpers.note_name_to_sct(note_name)
return (sct - 18367) / 200
def bottom_petal_to_rgb(petal, soft=0):
petal = int(petal) % 5
ret = None
if petal == 0:
ret = (1.0, 0.5, 0)
elif petal == 1:
ret = (0, 1.0, 1.0)
elif petal == 2:
ret = (0, 0, 1.0)
elif petal == 3:
ret = (1.0, 0, 1.0)
elif petal == 4:
ret = (0, 1.0, 0)
if soft > 0 and soft < 1:
return tuple([x * (1 - soft) + soft for x in ret])
else:
return ret
def interval_to_rgb(interval):
interval = int(interval) % 12
if interval == 0: # octave: neutral, light green
return (0.5, 1, 0.5)
if interval == 1: # flat 9th: spicy, purple
return (1, 0, 1)
if interval == 2: # 9th: mellow, blue
return (0, 0, 1)
if interval == 3: # minor 3rd: gritty warm, red
return (1.0, 0, 0)
if interval == 4: # major 3rd: warm, orange
return (1.0, 0.5, 0)
if interval == 5: # 4th: natural, green
return (0, 0.9, 0.3)
if interval == 6: # tritone, neon yellow
return (0.8, 1.0, 0)
if interval == 7: # 5th: reliable, teal
return (0, 0.7, 0.9)
if interval == 8: # augmented 5th: loud, cyan
return (0, 1.0, 1.0)
if interval == 9: # 13th: pink
return (1.0, 0.6, 0.5)
if interval == 10: # 7th: generic, lime green
return (0.0, 0.9, 0.3)
if interval == 11: # major 7th: peaceful, sky blue
return (0.7, 0.7, 1.0)
class Chord:
_triads = ["diminished", "minor", "major", "augmented", "sus2", "sus4"]
def __init__(self):
self._root = 0
self.triad = "major"
self.seven = False
self.nine = False
self.j = False
self.voicing = 0
self._num_voicings = 2
self.max_slew_rate = 0
@property
def root(self):
return self._root
@root.setter
def root(self, val):
if val > 12:
return
if val < -24:
return
self._root = val
def __repr__(self):
ret = tone_to_note_name(self.root)
while ret[-1].isdigit() or ret[-1] == "-":
ret = ret[:-1]
if self.triad == "augmented":
ret += " aug. "
elif self.triad == "diminished":
ret += " dim. "
elif self.triad == "minor":
ret += "-"
elif self.triad == "sus2":
ret += "sus2 "
elif self.triad == "sus4":
ret += "sus4 "
if self.seven:
if self.j:
ret += "j7 "
else:
ret += "7 "
if self.nine:
if self.nine == "#":
ret += "#9"
elif self.nine == "b":
ret += "b9"
else:
ret += "9"
if ret[-1] == " ":
ret = ret[:-1]
return ret
def triad_incr(self):
i = Chord._triads.index(self.triad)
i = (i + 1) % len(Chord._triads)
self.triad = Chord._triads[i]
def voicing_incr(self):
self.voicing = (self.voicing + 1) % self._num_voicings
def nine_incr(self):
if self.nine == "#":
self.nine = False
elif self.nine == "b":
self.nine = True
elif self.nine:
self.nine = "#"
else:
self.nine = "b"
def seven_incr(self):
if self.seven:
if self.j:
self.seven = False
self.j = False
else:
self.seven = True
self.j = True
else:
self.seven = True
self.j = False
@property
def notes(self):
chord = [0] * 5
if self.voicing == 0:
chord[0] = self.root
# TRIADE
if self.triad == "augmented":
chord[1] = self.root + 4
chord[2] = self.root + 8
chord[4] = self.root + 16
elif self.triad == "minor":
chord[1] = self.root + 3
chord[2] = self.root + 7
chord[4] = self.root + 15
elif self.triad == "diminished":
chord[1] = self.root + 3
chord[2] = self.root + 6
chord[4] = self.root + 15
elif self.triad == "sus2":
chord[1] = self.root + 2
chord[2] = self.root + 7
chord[4] = self.root + 14
elif self.triad == "sus4":
chord[1] = self.root + 5
chord[2] = self.root + 7
chord[4] = self.root + 17
else: # self.triad == "major":
chord[1] = self.root + 4
chord[2] = self.root + 7
chord[4] = self.root + 16
# SEVENTH
if self.seven:
if self.j:
chord[3] = self.root + 11
else:
chord[3] = self.root + 10
else:
chord[3] = self.root + 12
# NINETH
if self.nine:
if self.nine == "#":
chord[4] = self.root + 15
elif self.nine == "b":
chord[4] = self.root + 13
else:
chord[4] = self.root + 14
else:
if self.seven:
chord[4] = self.root + 12
elif self.voicing == 1:
# TRIADE
if self.triad == "augmented":
chord[0] = self.root + 4 - 12
chord[1] = self.root + 8 - 12
chord[4] = self.root + 16
elif self.triad == "minor":
chord[0] = self.root + 3 - 12
chord[1] = self.root + 7 - 12
chord[4] = self.root + 15
elif self.triad == "diminished":
chord[0] = self.root + 3 - 12
chord[1] = self.root + 6 - 12
chord[4] = self.root + 15
else: # self.triad == "major":
chord[0] = self.root + 4 - 12
chord[1] = self.root + 7 - 12
chord[4] = self.root + 16
# SEVENTH
if self.seven:
if self.j:
chord[2] = self.root + 11 - 12
chord[3] = self.root
chord[4] = chord[0] + 12
else:
chord[2] = self.root + 10 - 12
chord[3] = self.root
chord[4] = chord[0] + 12
else:
chord[2] = self.root
chord[3] = chord[0] + 12
chord[4] = chord[1] + 12
# NINETH
if self.nine:
if self.nine == "#":
chord[3] = self.root + 15 - 12
elif self.nine == "b":
chord[3] = self.root + 13 - 12
else:
chord[3] = self.root + 14 - 12
return chord
class HarmonicApp(Application): class HarmonicApp(Application):
def __init__(self, app_ctx: ApplicationContext) -> None: def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx) super().__init__(app_ctx)
self.color_intensity = 0.0
self.chord_index = 0 self.chord_index = 0
self.chord: List[int] = [] self.chord = None
self.synths = [blm.new(bl00mbox.patches.tinysynth_fm) for i in range(5)] self.chords = [Chord() for x in range(5)]
self.cp_prev = captouch.read()
for i, synth in enumerate(self.synths): self.cp_prev = captouch.read()
synth.signals.decay = 500 self.blm = None
synth.signals.waveform = -32767
synth.signals.attack = 50
synth.signals.volume = 0.3 * 32767
synth.signals.sustain = 0.9 * 32767
synth.signals.release = 800
synth.signals.fm_waveform = -32767
synth.signals.output = blm.mixer
# synth.fm = 1.5
self._set_chord(3)
self.prev_captouch = [0] * 10 self.prev_captouch = [0] * 10
self.fade = [0] * 5
self.mode = 0
self._set_chord(3, force_update=True)
self._num_modes = 3
self._file_settings = None
def _set_chord(self, i: int) -> None: def _try_load_settings(self, path):
hue = int(72 * (i + 0.5)) % 360 try:
if i != self.chord_index: with open(path, "r") as f:
return json.load(f)
except OSError as e:
if e.errno != errno.ENOENT:
raise # ignore file not found
def _try_write_default_settings(self, path: str, default_path: str) -> None:
with open(path, "w") as outfile, open(default_path, "r") as infile:
outfile.write(infile.read())
def _try_save_settings(self, path, settings):
try:
with open(path, "w+") as f:
f.write(json.dumps(settings))
f.close()
except OSError as e:
if e.errno != errno.ENOENT:
raise # ignore file not found
def _save_settings(self):
default_path = self.app_ctx.bundle_path + "/harmonic_demo-default.json"
settings_path = "/flash/harmonic_demo.json"
settings = self._try_load_settings(default_path)
assert settings is not None, "failed to load default settings"
file_is_different = False
user_settings = self._try_load_settings(settings_path)
if user_settings is None:
file_is_different = True
for i, chord in enumerate(settings["chords"]):
chord["triad"] = self.chords[i].triad
chord["j"] = self.chords[i].j
chord["seven"] = self.chords[i].seven
chord["nine"] = self.chords[i].nine
chord["root"] = self.chords[i].root
chord["voicing"] = self.chords[i].voicing
chord["tones_readonly"] = self.chords[i].notes
if not file_is_different:
user_chord = user_settings["chords"][i]
if self._file_settings is None:
file_is_different = True
else:
file_chord = self._file_settings["chords"][i]
for i in chord:
if chord.get(i) != file_chord.get(i):
file_is_different = True
break
if file_is_different:
self._try_save_settings(settings_path, settings)
self._file_settings = settings
def _load_settings(self):
default_path = self.app_ctx.bundle_path + "/harmonic_demo-default.json"
settings_path = "/flash/harmonic_demo.json"
settings = self._try_load_settings(default_path)
assert settings is not None, "failed to load default settings"
user_settings = self._try_load_settings(settings_path)
if user_settings is None:
self._try_write_default_settings(settings_path, default_path)
else:
settings.update(user_settings)
self._file_settings = settings
for i, chord in enumerate(settings["chords"]):
self.chords[i].triad = chord["triad"]
self.chords[i].j = chord["j"]
self.chords[i].seven = chord["seven"]
self.chords[i].nine = chord["nine"]
self.chords[i].root = chord["root"]
self.chords[i].voicing = chord["voicing"]
def _build_synth(self):
if self.blm is not None:
return
self.blm = bl00mbox.Channel("chord organ")
self.blm.volume = 32767
self.synths = [self.blm.new(chord_organ_synth) for i in range(5)]
for synth in self.synths:
synth.signals.output = self.blm.mixer
def _set_chord(self, i, force_update=False):
if i != self.chord_index or force_update:
self.chord_index = i self.chord_index = i
leds.set_all_hsv(hue, 1, 0.2) self.chord = self.chords[i]
leds.update() self.leds_rgb = bottom_petal_to_rgb(self.chord_index, soft=0)
self.chord = chords[i] self.hue_change = True
def draw(self, ctx: Context) -> None: def draw(self, ctx: Context) -> None:
i = self.color_intensity ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
ctx.rgb(i, i, i).rectangle(-120, -120, 240, 240).fill() ctx.line_width = 4
ctx.get_font_name(4)
ctx.text_align = ctx.CENTER
ctx.rgb(0, 0, 0) ctx.font_size = 20
ctx.scope()
for top_petal in range(5):
note_name = tone_to_note_name(self.chord.notes[top_petal])
interval = self.chord.notes[top_petal] - self.chord.root
color = interval_to_rgb(interval)
ctx.rgb(*color)
note_name = "".join([x for x in note_name if not x.isdigit()])
pos = 95 * cmath.exp(tai * (top_petal - 2.5) / 5)
end_pos = pos * (1 - 0.3j) * 1.27
start_pos = pos * (1 + 0.3j) * 1.27
mid_pos = (start_pos + end_pos) / 8
fade = self.fade[top_petal]
if fade > 0:
ctx.rgb(fade / 4, 0, fade / 4)
ctx.move_to(start_pos.imag, start_pos.real)
ctx.quad_to(mid_pos.imag, mid_pos.real, end_pos.imag, end_pos.real)
ctx.fill() ctx.fill()
ctx.rgb(*color)
ctx.move_to(start_pos.imag, start_pos.real)
ctx.quad_to(mid_pos.imag, mid_pos.real, end_pos.imag, end_pos.real)
ctx.stroke()
ctx.move_to(pos.imag * 1.07, pos.real * 1.07)
ctx.text(note_name)
if self.mode == 0:
ctx.save()
ctx.font_size = 25
ctx.rotate(math.tau * (3 / 5 - 1 / 4))
pos = -105
ctx.text_align = ctx.LEFT
for bottom_petal in range(5):
chord = self.chords[bottom_petal]
ctx.move_to(0, 0)
if bottom_petal == 3:
pos = -pos
ctx.text_align = ctx.RIGHT
ctx.rotate(-math.tau / 2)
ctx.rotate(-math.tau / 5)
if bottom_petal != self.chord_index:
ctx.rgb(*bottom_petal_to_rgb(bottom_petal, soft=0.3))
text = self.chords[bottom_petal].__repr__()
shift = 1
if len(text) > 8:
shift += 0.05 * (len(text) - 8)
if abs(pos * shift) > 120:
ctx.font_size = 20
shift = abs(120 / pos)
ctx.move_to(pos * shift, 0)
ctx.text(text)
ctx.font_size = 25
ctx.restore()
ctx.save()
ctx.font_size = 35
ctx.rotate(math.tau * (3 / 5 - 1 / 4))
pos = -90
ctx.text_align = ctx.LEFT
for bottom_petal in range(5):
# lazy
chord = self.chords[bottom_petal]
ctx.move_to(0, 0)
if bottom_petal == 3:
pos = -pos
ctx.text_align = ctx.RIGHT
ctx.rotate(-math.tau / 2)
ctx.rotate(-math.tau / 5)
if bottom_petal == self.chord_index:
ctx.rgb(*bottom_petal_to_rgb(bottom_petal))
text = self.chords[bottom_petal].__repr__()
shift = 1
if len(text) > 8:
shift += 0.05 * (len(text) - 8)
if abs(pos * shift) > 120:
shift = abs(120 / pos)
ctx.font_size = 30
ctx.move_to(pos * shift, 0)
ctx.text(text)
ctx.font_size = 35
ctx.restore()
if self.mode == 1:
ctx.font_size = 25
ctx.rgb(*bottom_petal_to_rgb(self.chord_index, soft=0.15))
ctx.save()
ctx.rotate(math.tau * (3 / 5 - 1 / 4))
pos = -105
ctx.text_align = ctx.LEFT
for bottom_petal in range(5):
ctx.move_to(0, 0)
if bottom_petal == 3:
ctx.text_align = ctx.RIGHT
pos = -pos
ctx.rotate(-math.tau / 2)
ctx.rotate(-math.tau / 5)
ctx.move_to(pos, 0)
if bottom_petal == 0:
ctx.text("oct+")
if bottom_petal == 4:
ctx.text("tone+")
if bottom_petal == 3:
ctx.text("tone-")
if bottom_petal == 1:
ctx.text("oct-")
ctx.restore()
ctx.move_to(0, 0)
ctx.font_size = 35
ctx.text(tone_to_note_name(self.chords[self.chord_index].root))
if self.mode == 2:
ctx.font_size = 20
ctx.rgb(*bottom_petal_to_rgb(self.chord_index, soft=0.15))
chord = self.chords[self.chord_index]
ctx.save()
ctx.rotate(math.tau * (3 / 5 - 1 / 4))
pos = -105
ctx.text_align = ctx.LEFT
for bottom_petal in range(5):
ctx.move_to(0, 0)
if bottom_petal == 3:
ctx.text_align = ctx.RIGHT
ctx.rotate(-math.tau / 2)
pos = -pos
ctx.rotate(-math.tau / 5)
ctx.move_to(pos, 0)
if bottom_petal == 4:
ctx.move_to(pos * 1.05, 0)
ctx.text("voice " + str(chord.voicing))
if bottom_petal == 0:
if chord.nine == "#":
ctx.text("#9")
elif chord.nine == "b":
ctx.text("b9")
elif chord.nine:
ctx.text("9")
else:
ctx.text("no9")
if bottom_petal == 1:
if chord.seven:
if chord.j:
ctx.text("j7")
else:
ctx.text("7")
else:
ctx.text("no7")
if bottom_petal == 3:
if chord.triad == "diminished":
ctx.text("dim.")
elif chord.triad == "augmented":
ctx.text("aug.")
else:
ctx.text(chord.triad)
ctx.restore()
ctx.move_to(0, 0)
text = self.chords[self.chord_index].__repr__()
if len(text) > 9:
ctx.font_size = 30
else:
ctx.font_size = 35
ctx.text(text)
def think(self, ins: InputState, delta_ms: int) -> None: def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms) super().think(ins, delta_ms)
if self.color_intensity > 0:
self.color_intensity -= self.color_intensity / 20 if self.blm is None:
cts = captouch.read() return
for i in range(10):
if cts.petals[i].pressed and (not self.cp_prev.petals[i].pressed): if self.input.buttons.app.left.pressed:
if i % 2: self.mode = (self.mode - 1) % self._num_modes
k = int((i - 1) / 2) elif self.input.buttons.app.right.pressed:
self._set_chord(k) self.mode = (self.mode + 1) % self._num_modes
else:
k = int(i / 2) cp = captouch.read()
self.synths[k].signals.pitch.tone = self.chord[k]
self.synths[k].signals.trigger.start() modulate_leds = False
self.color_intensity = 1.0 for i in range(0, 10, 2):
elif (not cts.petals[i].pressed) and self.cp_prev.petals[i].pressed: j = (10 - i) % 10
if (1 + i) % 2: if cp.petals[j].pressed:
k = int(i / 2) modulate_leds = True
self.synths[k].signals.trigger.stop() if not self.cp_prev.petals[j].pressed:
self.cp_prev = cts self.synths[i // 2].signals.pitch.tone = self.chord.notes[i // 2]
self.synths[i // 2].signals.trigger.start()
self.fade[i // 2] = 1
else:
if self.fade[i // 2] > 0:
self.fade[i // 2] -= self.fade[i // 2] * float(delta_ms) / 1000
if self.fade[i // 2] < 0.05:
self.fade[i // 2] = 0
if self.cp_prev.petals[j].pressed:
self.synths[i // 2].signals.trigger.stop()
for j in range(5):
if cp.petals[1 + 2 * j].pressed:
if not self.cp_prev.petals[1 + 2 * j].pressed:
if self.mode == 0:
self._set_chord((4 - j) % 5)
elif self.mode == 1:
if j == 0:
self.chord.root += 1
elif j == 1:
self.chord.root -= 1
elif j == 3:
self.chord.root -= 12
elif j == 4:
self.chord.root += 12
elif self.mode == 2:
if j == 3:
self.chord.seven_incr()
elif j == 4:
self.chord.nine_incr()
elif j == 1:
self.chord.triad_incr()
elif j == 0:
self.chord.voicing_incr()
if self.hue_change:
leds.set_slew_rate(min(self.max_slew_rate, 200))
leds.set_all_rgb(*self.leds_rgb)
self.hue_change = False
leds.update()
elif modulate_leds:
leds.set_slew_rate(min(self.max_slew_rate, 135))
h, s, v = colours.rgb_to_hsv(*self.leds_rgb)
led_index = int(random.random() * 39)
hue = h + 2 * (random.random() - 0.5)
leds.set_rgb(led_index, *colours.hsv_to_rgb(hue, s, v))
leds.update()
else:
leds.set_slew_rate(min(self.max_slew_rate, 135))
leds.set_all_rgb(*self.leds_rgb)
leds.update()
self.cp_prev = cp
def on_enter(self, vm: Optional[ViewManager]) -> None:
super().on_enter(vm)
self.mode = 0
if self.blm is None:
self._build_synth()
self.blm.foreground = True
self._load_settings()
self.max_slew_rate = leds.get_slew_rate()
leds.set_slew_rate(min(self.max_slew_rate, 130))
self._set_chord(self.chord_index, force_update=True)
def on_exit(self):
if self.blm is not None:
self.blm.free = True
self.blm = None
self._save_settings()
def get_help(self):
if self.mode == 0:
ret = (
"Page: Chord Switcher\n"
"(cycle with app button l/r)\n\n"
'This is intended as the "main playing field:" '
"You can use the bottom petals to select a chord "
"and the top petals to play notes from that chord."
)
elif self.mode == 1:
ret = (
"Page: Root Shifter\n"
"(cycle with app button l/r)\n\n"
"You can use the bottom petals to shift the root "
"of the current chord by either semitones (petal 1+3) "
"or whole octaves (petal 7+9). The center label "
"indicates current root note and octave in scientific "
"pitch notation.\n\n"
"Tip: If you find yourself with two chord shapes that "
"you like but don't quite fit together try shifting "
"them relative to each other."
)
elif self.mode == 2:
ret = (
"Page: Chord Shaper\n"
"(cycle with app button l/r)\n\n"
"The bottom petals cycle through different abstract "
"qualities that set the shape of the current chord:\n\n"
"Petal 1: Voicing\n"
"The voicing refers to the arrangement of chord tones: "
"A chord tone may be present in any octave, or multiple "
"ones, or none at all (sometimes). Voicings can change the "
'"feel" of a chord while often retaining '
"the (dis)harmonic relationship with other chords.\n\n"
"Petal 3: Triad\n"
"Triads is probably the most well-known concept about chords: "
"Major and minor chords are triads, but there's a few more. "
"These are often used as a foundation "
'and then "spiced up" with additional "tension" notes. Petals 7+9 '
'are responsible for those; if they are set to "no7" and "no9" '
"respectively you'll get only the basic triad.\n\n"
"Petal 7: 7th\n"
"The 7th comes in 2 forms, the regular kind which goes well "
"which just about everything and the major 7th (denoted as j7) "
"may result in very bright and/or sharp shapes.\n\n"
"Petal 9: 9th\n"
"The 9th comes in 3 forms: As with the 7th the regular one is "
"fairly mellow while the b9 is notoriously dramatic in just "
"about any context. The #9 is a strange one as it is tonally "
"identical to the minor 3rd: With minor or diminished triads "
"(which have that tone in the triad) it doesn't add much, but "
'with major or augmented it can have a "double triad" effect.\n\n'
"There's a lot more tensions out there in the wild, and also "
"entirely different concepts to build harmony with, this merely "
"aims to be a compact example of chord building.\n\n"
"The notation used for chords is a bit simplified in order to "
"make it a bit more intuitive; the real world is full of special "
"names and shorthands that may make pattern recognition harder "
"so we're not using most of them here."
)
ret += (
"\n\nYour settings are saved to and loaded from "
"/flash/sys/harmonic_demo.json when exiting and entering. "
"Note for developers: Saving on flash is bad practice, do not replicate "
"(see Documentation.) This will be moved in the future."
)
return ret
if __name__ == "__main__":
from st3m.run import run_app
run_app(HarmonicApp, "/flash/sys/apps/demo_harmonic")
[app] [app]
name = "Harmonic" name = "chord organ"
menu = "Music" category = "Music"
[entry] [entry]
class = "HarmonicApp" class = "HarmonicApp"
......
{
"chords": [
{ "triad": "major", "root": -4, "j": true, "voicing": 0, "nine": true, "seven": false},
{ "triad": "major", "root": 5, "j": false, "voicing": 1, "nine": true, "seven": false},
{ "triad": "major", "root": 7, "j": false, "voicing": 1, "nine": false, "seven": true},
{ "triad": "minor", "root": 0, "j": false, "voicing": 0, "nine": true, "seven": false},
{ "triad": "major", "root": 3, "j": true, "voicing": 0, "nine": false, "seven": true}
]
}