Skip to content

Instantly share code, notes, and snippets.

@todbot
Last active October 4, 2024 13:27
Show Gist options
  • Save todbot/7dc50b8de8db03638a9a291e9132b488 to your computer and use it in GitHub Desktop.
Save todbot/7dc50b8de8db03638a9a291e9132b488 to your computer and use it in GitHub Desktop.
use rotary encoder to launch balls that play notes when bouncing in CircuitPython
# synthy-balls.py -- use rotary encoder to launch balls that play notes when bouncing
# 20 Aug 2024 - @todbot / Tod Kurt
# video demo: https://youtu.be/cCTPtk6KQQk
import time, random, math
import board
import busio, displayio, terminalio
import rotaryio, keypad
import audiobusio, audiocore, audiomixer, synthio
import gc9a01
#from adafruit_display_text import bitmap_label as label
import vectorio
import rainbowio
encoder = rotaryio.IncrementalEncoder(board.GP16, board.GP17)
keys = keypad.Keys( (board.GP9,), value_when_pressed=False)
dw,dh = 240,240
displayio.release_displays()
spi = busio.SPI(clock=board.GP14, MOSI=board.GP15)
display_bus = displayio.FourWire(spi, command=board.GP12, reset=board.GP13, baudrate=32_000_000)
display = gc9a01.GC9A01(display_bus, width=dw, height=dh, auto_refresh=False)
maingroup = displayio.Group()
display.root_group = maingroup
i2s_bclk, i2s_lclk, i2s_data = board.GP20, board.GP21, board.GP22
audio = audiobusio.I2SOut(bit_clock=i2s_bclk, word_select=i2s_lclk, data=i2s_data)
mixer = audiomixer.Mixer(voice_count=1, channel_count=1, sample_rate=44100, buffer_size=2048)
audio.play(mixer)
synth = synthio.Synthesizer(channel_count=1, sample_rate=44100)
mixer.voice[0].play(synth)
amp_env = synthio.Envelope(attack_time=0.02, attack_level=1, sustain_level=1, release_time=1)
synth.envelope = amp_env
scale_map_mixolydian = (
0, # 0
0, # 1
2, # 2
2, # 3
4, # 4
5, # 5
7, # 6
7, # 7
9, # 8
10, # 9
10, # 10
12, # 11
)
num_colors = 64
bal_pal = displayio.Palette(num_colors)
bal_pal.make_transparent(0)
for i in range(1,num_colors):
bal_pal[i] = rainbowio.colorwheel( int(i * 255/num_colors) )
class Ball:
def __init__(self, x,y, r=10, vx=0, vy=0, c=1):
self.x, self.y, self.r = x,y,r
self.vx, self.vy = vx,vy
self.obj = vectorio.Circle(pixel_shader=bal_pal, radius=r, x=int(x), y=int(y), color_index=c)
self.bounds = bounds #(xmin,ymin, xmax,ymax)
self.bounced = False
def update(self):
self.x = self.x + self.vx
self.y = self.y + self.vy
self.bounced = False
if self.x <= self.bounds[0] or self.x >= self.bounds[2]:
self.vx = - self.vx
self.bounced = True
if self.y <= self.bounds[1] or self.y >= self.bounds[3]:
self.vy = - self.vy
self.bounced = True
self.x = min(max(self.x, self.bounds[0]), self.bounds[2])
self.y = min(max(self.y, self.bounds[1]), self.bounds[3])
self.obj.x = int(self.x)
self.obj.y = int(self.y)
def map_to_scale(scale_map, n):
octave = n // 12
interval = n % 12
interval = scale_map[interval]
return octave * 12 + interval
# indicator to show where the new balls will launch from
circle = vectorio.Circle(pixel_shader=bal_pal, radius=20, x=120, y=120, color_index=32)
maingroup.append(circle)
circleO = vectorio.Circle(pixel_shader=bal_pal, radius=16, x=120, y=120, color_index=0)
maingroup.append(circleO)
ballgroup = displayio.Group()
maingroup.append( ballgroup)
bounds = (30,30, 209, 209)
balls = []
a = 0
last_encoder_pos = encoder.position
last_debug_time = 0
notes = []
while True:
denc = encoder.position - last_encoder_pos
a = a + denc*10
last_encoder_pos = encoder.position
a = a % 360
x = 100 * math.cos(a * 2*math.pi/360 )
y = 100 * math.sin(a * 2*math.pi/360 )
circle.x, circle.y = int(120+x), int(120+y)
circle.color_index = 1 + int(a/360 * num_colors)
circleO.x, circleO.y = circle.x, circle.y
if key := keys.events.get():
print(key)
if key.pressed:
ball = Ball(120+x, 120+y, 15, c=circle.color_index)
ball.vx = -x/20
ball.vy = -y/20
ball.bounds = bounds
ball.notenum = map_to_scale( scale_map_mixolydian, int( (a/360 * 64) + 32) )
balls.append(ball)
ballgroup.append(ball.obj)
if len(ballgroup) > 12: # max voices
ballgroup.pop(0)
balls.pop(0)
print("POP!")
for i, ball in enumerate(balls):
ball.update()
if ball.bounced:
#print("BOUNCE",i)
note = synthio.Note(synthio.midi_to_hz(ball.notenum))
synth.press( note )
synth.release( note )
if time.monotonic() - last_debug_time > 0.1:
last_debug_time = time.monotonic()
print("hi", time.monotonic(), "spi:", spi.frequency, "enc:", encoder.position, denc, a, x,y)
display.refresh( target_frames_per_second = 20 )
@todbot
Copy link
Author

todbot commented Sep 6, 2024

Video demo on Pico2 RP2350: https://youtu.be/cCTPtk6KQQk

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment