# HB0110-synthy-balls.py
# modified from Tod Kurt's synthy-balls.py
# use rotary encoder to launch balls that play notes when bouncing
 
# Tod's original header:
# 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
import vectorio
import rainbowio

# right rotary encoder
right_encoder = rotaryio.IncrementalEncoder(board.GP15, board.GP14)
right_key = keypad.Keys( (board.GP7,), value_when_pressed=False)

# left rotary encoder
left_encoder = rotaryio.IncrementalEncoder(board.GP16, board.GP17)
left_key = keypad.Keys( (board.GP18,), value_when_pressed=False)

dw,dh = 240,240
displayio.release_displays()
spi = busio.SPI(clock=board.GP10, MOSI=board.GP11)
display_bus = displayio.FourWire(spi, command=board.GP8, chip_select=board.GP9, reset=board.GP12, 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.GP3, board.GP4, board.GP5
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 = right_encoder.position
last_debug_time = 0
notes = []

while True:
    denc = right_encoder.position - last_encoder_pos
    a = a + denc*10
    last_encoder_pos = right_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 := right_key.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!")
                
    if key := left_key.events.get():
        print(key)
        if key.pressed:
            if len(ballgroup) > 0: 
                ballgroup.pop(0)
                balls.pop(0)
   
    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:", right_encoder.position, denc, a, x,y)

    display.refresh( target_frames_per_second = 20 )

