Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<a href="https://www.tindie.com/stores/zodiacdesigns/?ref=offsite_badges&utm_source=sellers_ZodiacDesigns&utm_medium=badges&utm_campaign=badge_large"><img src="https://d2ss6ovg47m0r5.cloudfront.net/badges/tindie-larges.png" alt="I sell on Tindie" width="200" height="104"></a>
<a href="https://www.zodiaccurios.com.au/shop/p/spectrogram-mini">

# Spectrogram

Expand Down
1 change: 0 additions & 1 deletion hardware/pcb/~Spectrogram flex pcb.kicad_sch.lck

This file was deleted.

51 changes: 51 additions & 0 deletions software/micropython/colour_lut_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#This was the Claude sonnet 4.5 builder for the LUT
def create_color_lut():
lut = []
for i in range(256):
if i <= 170:
progress = i / 170.0
r = int(255 * progress)
g = 0
b = int(255 * (1 - progress))
else:
progress = (i - 171) / 84.0
r = 255
g = int(255 * progress)
b = 0
lut.append((r, g, b))
return lut

#this is astrolabe's generalised LUT builder
#color_list must be same length as colour_stop_positions. positions must be ascending order. range top must be = top position
def generalised_color_lut(color_list, color_stop_positions, range_top_value, step_size):
lut = []
color_pairs = []
stop_pairs = []

for i in range(len(color_list)-1):
color_pairs.append([color_list[i],color_list[i+1]])
stop_pairs.append([color_stop_positions[i],color_stop_positions[i+1]])
# print("colour_pairs",color_pairs)
# print("stop_pairs",stop_pairs)

start_val=0
for index, pair in enumerate(color_pairs):
# progress=#0-1 value
# print(index, pair)
for i in range(start_val,range_top_value,step_size): #I have constructed this to work with 255 color values. For LUTS that step at 1 unit oer step, it looks algood.
#for LUTS that step at greater steps, more thought is required when calling them: e.g., calling with step_size=256/4 will not perfectly map from start to end (the last colour step will be missing) Therefor, call with correspondingly coarser steps. (n-1)
if i<=stop_pairs[index][1]:
progress= (i-stop_pairs[index][0])/(stop_pairs[index][1]-stop_pairs[index][0]) #progress is a 0-1 parameterized value within each stop range
# print(progress)
#base value + delta (can be +ve or -ve) * progress fraction of delta's application across range
r=int(pair[0][0]+((pair[1][0]-pair[0][0])*progress))
g=int(pair[0][1]+((pair[1][1]-pair[0][1])*progress))
b=int(pair[0][2]+((pair[1][2]-pair[0][2])*progress))
# print(r,g,b)
lut.append((r, g, b))
else:
# print('next colour transition/stop range')
start_val=i
break #break out so the start_val isn't continuously updated as the loops runs out normally
#
return lut
118 changes: 118 additions & 0 deletions software/micropython/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
TEST='test'

FLIP_DISPLAY=True #NOT IMPLIMENTED, LOL #True: high freqencies on left, low on the right - False: vice versa

#presets:
size='s' #(s,m,l,c) #small 7px, medium 12px, large 18/24px, c=custom, go wild
color_blind=False #True/False

if size=='s':
NUM_LEDS = 7
#'Resolution'=notes_per_LED: [1,2,3,4,6,12]
BOOT_RESOLUTION_INDEX=5

if size=='m':
#goal: make this arbitrarily small/long & accurately reflected in setting. No funny +1 business.
NUM_LEDS = 12 #50 #actually needs to be number of leds+1# ugly work around for array out of bound error caused by ring buffer in mic.py(??)
#'Resolution'=notes_per_LED: [1,2,3,4,6,12]
BOOT_RESOLUTION_INDEX=4


#configure touch thresholds:
UNPRESSED_CAPACITIVE_READING=80000
PRESSED_CAPACITIVE_READING=90000
#print out the touch levels and compare to the below thresholds.
PRINT_TOUCH_READING=True

#these are the hues the maker selected for the 12 notes of an octave, you can change them :)
import colour_lut_builder as clb
if color_blind==True:
#viridis for replacing intensity for color blind folks:
INTENSITY_COLOR_LUT=clb.generalised_color_lut([(0,0,0),(0,0,255),(0,255,0),(255,255,0)],[0,63,191,255],256,1)
#plasma note scheme
SYN_NOTE_HUES=clb.generalised_color_lut([(0,0,255),(255,0,0),(255,255,0),(255,255,255)],[0,63,211,255],256,22)

#Bunch of tests: you can go crazy trying to pick colours, particularly when you can't 'see' them.
#scheme build call for plasma synesthesia
#plasma0: missing the white top end, which will allow better determination
#clb.generalised_color_lut([(0,0,255),(255,0,0),(255,255,0)],[0,170,255],256,20)
#plasma1: missing the white top end, which will allow better determination. White further fits in with the perceptual brightness angle of these colour schemes
#clb.generalised_color_lut([(0,0,255),(255,0,0),(255,255,0),(255,255,255)],[0,63,191,255],256,20)
#plasma2: yellows not distinct: moving stops. Yellow ususally a small end band
#clb.generalised_color_lut([(0,0,255),(255,0,0),(255,255,0),(255,255,255)],[0,63,211,255],256,22)
#plasma3 adding green denuemont to white, to move closer to blue/a cyclical clour scheme
#clb.generalised_color_lut([(0,0,255),(255,0,0),(255,255,0),(255,255,255),(0,255,0)],[0,60,120,180,255],256,22)
#plasma4 still messing
#clb.generalised_color_lut([(0,0,255),(255,0,0),(255,255,0),(255,255,255),(0,255,0),(0,255,255)],[0,60,120,150,200,255],256,22)
#plasma5: just adding sharps manually. 256/7=36.57
#clb.generalised_color_lut([(0,0,255),(255,0,0),(255,255,0),(255,255,255)],[0,63,211,255],256,36)
#which yeilds: [(0, 0, 255), (145, 0, 109), (255, 15, 0), (255, 77, 0), (255, 139, 0), (255, 201, 0), (255, 255, 28), (255, 255, 237)]

else:
#plama scheme
INTENSITY_COLOR_LUT=clb.generalised_color_lut([(0,0,255),(255,0,0),(255,255,0)],[0,170,255],256,1)
#rainbow note scheme - manually selected
SYN_NOTE_HUES=[(255,0,0),(255,30,30),(255,60,0),(255,255,0),(255,255,30),(0,255,0),(80,220,10),(0,155,255),(0,0,255),(50,0,255),(255,0,255),(255,255,255)]

DEV_STATUS_LED_PIN=21

#pipedream
HALVED_MIRRORED_SPECTRUM=False
MIRROR_START_INDEX=NUM_LEDS/2

#Colour selections:


#this can be used to change the direction of the waterfall. Depends on one's prefered viewing angle Normally pin0=6,pin1=8,pin2=7
LEDS_PIN0 = 6
LEDS_PIN1 = 8
LEDS_PIN2 = 7

ID = 0 #I2S identity
#mic pins
SD = 11
SCK = 10
WS = 9

#Boot visualisation settings/options:

#integer multiples of 10: logic/resolution is set in menu.py
BOOT_MAX_DB=-40
BOOT_MIN_DB=-80


#menu display stuff
MENU_LED_OFFSET=0 #useful for (e.g.) 18 or 24 wide menus that only want 12 pixel wide displays
MENU_SIZE=NUM_LEDS-MENU_LED_OFFSET-1 #or face crash #counting from zero... LEDS are indexed from 0... #ideally should be tied to LEDS-1
MENU_SCALE=1 #not used?? useful for crunching down the menu's size (1 pixel=?, 6pixels=0.5, 12pixels=1

BOOT_BRIGHTNESS_INDEX=1
BRIGHTNESS_OPTIONS=[2,6,16,32,64,128,255] #this is semi-independent of the menu width, to make it line up, have the same number of entries as the menu size. If you go outside the menu size, the top brightness pixel isn't displayed.
#two hours of manual fitting by eye and this was the screamingly obvious final outcome: for 7 pixels=[4,8,16,32,64,128,255] I modified the low blues to get that "blue LED not on anymore" vibe
#[2,7,28,64,113,177,255] for 7 pixels, calculated as a square relationship increase in brightness (doesn't look good though lol)
#for 12 pixels, i guessed/input:[2,3,4,5,7,10,20,35,50,90,160,255]

DB_RANGE=120
DB_STEP_SIZE=5#[10db, 5db, 1db]
from math import ceil
DB_SETTINGS_PER_BIN=s if (s := ceil((DB_RANGE/DB_STEP_SIZE)/NUM_LEDS)) <= NUM_LEDS else None #walrus operator, lol
DB_COARSE_STEP_SIZE=DB_STEP_SIZE*DB_SETTINGS_PER_BIN

DB_ACTIVE_BRIGHTNESS_BUMP=100 #indicate active status with brightness, not colour

print("DB_SETTINGS_PER_BIN",DB_SETTINGS_PER_BIN)
#helpful if the stepsize is a muliple of the settings_per_bin. Or just use //
if DB_SETTINGS_PER_BIN>1:
if color_blind==True:
DB_INDICATOR_COLORS=clb.generalised_color_lut([(255,255,000),(000,255,000),],[0,255],256,256//(DB_SETTINGS_PER_BIN-1)) #-1 is for coarser steps to get full range mapped #shades of yellow/green
else:
DB_INDICATOR_COLORS=clb.generalised_color_lut([(255,255,000),(000,255,000),],[0,255],256,256//(DB_SETTINGS_PER_BIN-1)) #-1 is for coarser steps to get full range mapped #shades of yellow/green
print(DB_INDICATOR_COLORS)
else:
if color_blind==True:
DB_INDICATOR_COLORS=[(255,000,000)] #active=red - stands out from blue.
pass
else:
DB_INDICATOR_COLORS=[(000,255,000)] #active=green


28 changes: 15 additions & 13 deletions software/micropython/leds.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import config
import asyncio
from random import randint
from neopixel import NeoPixel
from machine import Pin
from time import ticks_us, ticks_diff

NUM_LEDS = 13 #50 #ugly work around for array out of bound error caused by ring buffer in mic.py
DEV_STATUS_LED_PIN=21
LEDS_PIN0 = 6
LEDS_PIN1 = 8
LEDS_PIN2 = 7

class Leds():
def __init__(self):
gpioS = Pin(DEV_STATUS_LED_PIN, Pin.OUT)
gpio0 = Pin(LEDS_PIN0, Pin.OUT)
gpio1 = Pin(LEDS_PIN1, Pin.OUT)
gpio2 = Pin(LEDS_PIN2, Pin.OUT)
gpioS = Pin(config.DEV_STATUS_LED_PIN, Pin.OUT)
gpio0 = Pin(config.LEDS_PIN0, Pin.OUT)
gpio1 = Pin(config.LEDS_PIN1, Pin.OUT)
gpio2 = Pin(config.LEDS_PIN2, Pin.OUT)
self.status_pix = NeoPixel(gpioS, 1, )
self.neopix0 = NeoPixel(gpio0, NUM_LEDS)
self.neopix1 = NeoPixel(gpio1, NUM_LEDS)
self.neopix2 = NeoPixel(gpio2, NUM_LEDS)
self.neopix0 = NeoPixel(gpio0, config.NUM_LEDS)
self.neopix1 = NeoPixel(gpio1, config.NUM_LEDS)
self.neopix2 = NeoPixel(gpio2, config.NUM_LEDS)
self.led_list=[self.neopix0,self.neopix1,self.neopix2,self.status_pix]

def __iter__(self):
Expand All @@ -41,6 +36,10 @@ async def light(self, led_nr, values):
async def dance(self):
await self.blink()

async def show_rgb(self, led_arr_num, led_nr, rgb):
self.led_list[led_arr_num][led_nr] = rgb


async def show_hsv(self, led_arr_num, led_nr, hue, sat, val):
#show_hsv time to pixel: 3068 µs
t0 = ticks_us()
Expand All @@ -53,6 +52,9 @@ async def show_hsv(self, led_arr_num, led_nr, hue, sat, val):
async def write(self, led_arr_num):
self.led_list[led_arr_num].write()

async def fill(self, led_arr_num, colour):
self.led_list[led_arr_num].fill((colour))

#apparently not smooth
async def fade_rgb(self, led_arr_num, led_nr, target_hue, steps=30):
current_rgb = self.led_list[led_arr_num][led_nr]
Expand Down
38 changes: 32 additions & 6 deletions software/micropython/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,46 @@
from debug import set_global_exception
from touch import Touch
from menu import Menu
import utime

class Watchdog:
def __init__(self, check_interval_ms=100, alert_threshold_ms=200):
self.task_heartbeats={}
self.check_interval_ms=100
self.alert_threshold_ms=200

def heartbeat(self, taskname):
now=utime.ticks_ms()
self.task_heartbeats[taskname]=now


async def watch(self):
"""Monitor all registered tasks for starvation"""
while True:
await asyncio.sleep_ms(self.check_interval_ms)
now = utime.ticks_ms()

for task_name, last_heartbeat in self.task_heartbeats.items():
elapsed = utime.ticks_diff(now, last_heartbeat)
# print(f"{task_name} {elapsed}ms")
if elapsed > self.alert_threshold_ms:
print(f"⚠️ {task_name} hasn't run in {elapsed}ms!")


async def main():
#set_global_exception()
touch0 = Touch(Pin(4))
touch1 = Touch(Pin(3))
touch2 = Touch(Pin(2))
microphone = Mic()
menu = Menu(microphone)
watchdog = Watchdog() #must be passed to each class object if being used.
touch0 = Touch(watchdog,Pin(4))
touch1 = Touch(watchdog,Pin(3))
touch2 = Touch(watchdog,Pin(2))
microphone = Mic(watchdog)
menu = Menu(watchdog,microphone)
menu.add_touch(touch0)
menu.add_touch(touch1)
menu.add_touch(touch2)

#print("Starting main gather...")
await asyncio.gather(touch0.start(), touch1.start(), touch2.start(), menu.start(), microphone.start())
await asyncio.gather(watchdog.watch(),touch0.start(), touch1.start(), touch2.start(), menu.start(), microphone.start())
#await asyncio.gather(microphone.start())
try:
asyncio.run(main())
Expand Down
Loading