Skip to content

Commit

Permalink
juno6,voices,midi.py: Keep Juno6 patch in-sync across both places to …
Browse files Browse the repository at this point in the history
…change it, preserve juno6 edits.
  • Loading branch information
dpwe committed May 28, 2024
1 parent 38e3dc0 commit 4174588
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 49 deletions.
9 changes: 9 additions & 0 deletions tulip/shared/py/arpegg.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ def program_change(self, patch_number):
def amy_voices(self):
return self.synth.amy_voices

@property
def num_voices(self):
return self.synth.num_voices

@property
def patch_number(self):
return self.synth.patch_number
Expand Down Expand Up @@ -163,3 +167,8 @@ def get_new_voices(self, num_voices):
def release_voices(self):
return self.synth.release_voices()

def get_patch_state(self):
return self.synth.get_patch_state()

def set_patch_state(self, state):
return self.synth.set_patch_state(state)
95 changes: 60 additions & 35 deletions tulip/shared/py/juno6.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# juno6.py
# A more pure-LVGL (using Tulip's UIScreen) UI for Juno-6
from tulip import UIScreen, UIElement, pal_to_lv, lv_depad, lv, midi_in, midi_add_callback, midi_remove_callback, seq_ppq, seq_add_callback, seq_remove_callback, music_map
import tulip
import lvgl as lv
#from tulip import UIScreen, UIElement, pal_to_lv, lv_depad, lv, midi_in, midi_add_callback, midi_remove_callback, seq_ppq, seq_add_callback, seq_remove_callback, music_map
import time


class JunoSection(UIElement):
class JunoSection(tulip.UIElement):
"""A group of elements in an red-header group with a title."""

header_color = 224
Expand All @@ -24,8 +26,8 @@ def __init__(self, name, elements=None, header_color=None):
self.header = lv.label(self.group)
self.header.set_text(name)
self.header.set_style_text_font(self.header_font, lv.PART.MAIN)
self.header.set_style_bg_color(pal_to_lv(self.header_color), lv.PART.MAIN)
self.header.set_style_text_color(pal_to_lv(self.text_color), lv.PART.MAIN)
self.header.set_style_bg_color(tulip.pal_to_lv(self.header_color), lv.PART.MAIN)
self.header.set_style_text_color(tulip.pal_to_lv(self.text_color), lv.PART.MAIN)
self.header.set_style_bg_opa(lv.OPA.COVER, lv.PART.MAIN)
self.header.align_to(self.group, lv.ALIGN.TOP_LEFT, 0, 0)
self.header.set_style_text_align(lv.TEXT_ALIGN.CENTER,0)
Expand All @@ -43,20 +45,20 @@ def add(self, objs, direction=lv.ALIGN.OUT_RIGHT_MID):
obj.group.align_to(self.last_obj_added, direction, 0, 0)
else:
obj.group.align_to(self.header, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 0)
obj.group.set_style_bg_color(pal_to_lv(self.bg_color), lv.PART.MAIN)
obj.group.set_style_bg_color(tulip.pal_to_lv(self.bg_color), lv.PART.MAIN)

self.last_obj_added = obj.group
total_width += obj.group.get_width()

self.header.set_width(total_width)
lv_depad(self.header)
tulip.lv_depad(self.header)

self.group.set_width(total_width + self.section_gap)
lv_depad(self.group)
tulip.lv_depad(self.group)
self.group.remove_flag(lv.obj.FLAG.SCROLLABLE)


class JunoButtons(UIElement):
class JunoButtons(tulip.UIElement):
"""A set of buttons/checkboxes, one under another."""

font = lv.font_unscii_8
Expand Down Expand Up @@ -145,7 +147,7 @@ def prev(self):
self.next(step=-1)


class JunoSlider(UIElement):
class JunoSlider(tulip.UIElement):
"""Builds a slider with description on top and a ganged label on bottom showing the value."""

handle_color = 0
Expand Down Expand Up @@ -175,9 +177,9 @@ def __init__(self, name, callback=None):
self.slider.set_style_bg_opa(lv.OPA.COVER, lv.PART.MAIN)
self.slider.set_width(JunoSlider.width)
self.slider.set_height(JunoSlider.height)
self.slider.set_style_bg_color(pal_to_lv(JunoSlider.bar_color), lv.PART.INDICATOR)
self.slider.set_style_bg_color(pal_to_lv(JunoSlider.bar_color), lv.PART.MAIN)
self.slider.set_style_bg_color(pal_to_lv(JunoSlider.handle_color), lv.PART.KNOB)
self.slider.set_style_bg_color(tulip.pal_to_lv(JunoSlider.bar_color), lv.PART.INDICATOR)
self.slider.set_style_bg_color(tulip.pal_to_lv(JunoSlider.bar_color), lv.PART.MAIN)
self.slider.set_style_bg_color(tulip.pal_to_lv(JunoSlider.handle_color), lv.PART.KNOB)
self.slider.set_style_radius(JunoSlider.handle_radius, lv.PART.KNOB)
self.slider.set_style_pad_ver(JunoSlider.handle_v_pad, lv.PART.KNOB)
self.slider.set_style_pad_hor(JunoSlider.handle_h_pad, lv.PART.KNOB)
Expand Down Expand Up @@ -214,7 +216,7 @@ def cb(self, e):
if self.callback:
self.callback(self.slider.get_value() / 127.0)

class JunoControlledLabel(UIElement):
class JunoControlledLabel(tulip.UIElement):
"""A label with some press-to-act buttons (e.g. + and -)."""
button_size = 28
button_space = 4
Expand Down Expand Up @@ -291,9 +293,9 @@ def value_up(self):
def value_down(self):
self.value_delta(-1)

def set_value(self, value):
def set_value(self, value, additional_kwargs={}):
self.value = value
self.name = self.set_fn(self.value+self.offset)
self.name = self.set_fn(self.value + self.offset, **additional_kwargs)
self.set_text(self.name)


Expand All @@ -309,7 +311,7 @@ def hexify(bytelist):
return ' '.join('%02x' % b for b in bytelist)


def set_patch_with_state(jp, patch_num, midi_channel):
def update_patch_including_state(jp, patch_num, midi_channel):
"""Mutates juno_patch in-place to use patch_num, or override by state for midi_chan if any."""
jp.set_patch(patch_num)
# Maybe this channel has existing modified state?
Expand All @@ -326,9 +328,10 @@ def set_patch_with_state(jp, patch_num, midi_channel):
if patch_num is not None and patch_num < 128:
jp = juno.JunoPatch() # .from_patch_number(patch_num)
jp.set_voices(amy_voices)
set_patch_with_state(jp, patch_num, m_channel)
update_patch_including_state(jp, patch_num, m_channel)
juno_patch_for_midi_channel[m_channel] = jp


def current_juno():
global midi_channel
jp = juno_patch_for_midi_channel.get(midi_channel, None)
Expand Down Expand Up @@ -396,7 +399,8 @@ def cho(n):
ch = JunoSection("CH", [chorus_mode := JunoRadioButtons("Mode", ["Off", "I", "II", "III"],
[cho(0), cho(1), cho(2), cho(3)])])

def setup_from_patch(patch):

def setup_ui_from_juno_patch(patch):
"""Make the UI match the values in a JunoPatch."""
current_juno().defer_param_updates = True
glob_fns = globals()
Expand All @@ -420,24 +424,30 @@ def setup_from_patch(patch):
return patch.name



def setup_from_patch_number(patch_number):
def setup_from_patch_number(patch_number, propagate_to_voices_app=True):
global midi_channel
# See how many voices are allocated going in.
_, amy_voices = midi.config.channel_info(midi_channel)
# Use no fewer than 4.
num_amy_voices = 0 if amy_voices == None else len(amy_voices)
polyphony = max(4, num_amy_voices)
music_map(midi_channel, patch_number, polyphony)
tulip.music_map(midi_channel, patch_number, polyphony)
_, amy_voices = midi.config.channel_info(midi_channel)
jp = juno.JunoPatch() #.from_patch_number(patch_number)
#jp = juno.JunoPatch() #.from_patch_number(patch_number)
#juno_patch_for_midi_channel[midi_channel] = jp
jp = juno_patch_for_midi_channel[midi_channel]
jp.set_voices(amy_voices)
set_patch_with_state(jp, patch_number, midi_channel)
juno_patch_for_midi_channel[midi_channel] = jp
update_patch_including_state(jp, patch_number, midi_channel)
setup_ui_from_juno_patch(jp)
# Maybe inform the voices app.
if propagate_to_voices_app:
try:
voices_app = tulip.running_apps.get("voices", None)
voices_app.patchlist.select(patch_number)
except:
pass
return jp.name

#current_juno().patch_number = patch_number
#current_juno().name = setup_from_patch(jp)
return current_juno().name

def setup_from_midi_chan(new_midi_channel):
"""Switch which JunoPatch we display based on MIDI channel."""
Expand All @@ -461,14 +471,25 @@ def setup_from_midi_chan(new_midi_channel):
else:
#print("new patch patch is %d" % (new_patch.patch_number))
new_patch.init_AMY()
patch_selector.value = new_patch.patch_number # Bypass actually reading that patch, just set the state.
patch_selector.set_text(new_patch.name)
setup_from_patch(new_patch)
try:
patch_selector.value = new_patch.patch_number # Bypass actually reading that patch, just set the state.
patch_selector.set_text(new_patch.name)
except:
# patch_selector isn't created yet.
pass
setup_ui_from_juno_patch(new_patch)
return "MIDI chan %d" % (midi_channel)

patch_selector = JunoTokenSpinbox('Patch', set_fn=setup_from_patch_number, initial_value=current_juno().patch_number)
midi_selector = JunoTokenSpinbox('MIDI', set_fn=setup_from_midi_chan, max_value=15, width=160, offset=1)
patch_selector = JunoTokenSpinbox('Patch', set_fn=setup_from_patch_number, initial_value=current_juno().patch_number)


# Hook for voices.py to change the patch.
def update_patch_for_channel(channel, patch_num):
global midi_channel
if channel == midi_channel:
patch_selector.set_value(patch_num,
additional_kwargs={'propagate_to_voices_app': False})



Expand Down Expand Up @@ -545,7 +566,7 @@ def control_change(control, value):


def midi_event_cb(x):
m = midi_in()
m = tulip.midi_in()
while m is not None and len(m) > 0:
if m[0] == 0xb0: # Other control slider.
control_change(m[1], m[2])
Expand All @@ -557,21 +578,25 @@ def midi_event_cb(x):
# Are there more events waiting?
m = m[3:]
if len(m) == 0:
m = midi_in_fn()
m = tulip.midi_in()


def quit(screen):
state = current_juno().to_sysex()
#print("quit: saving state for channel %d: %s" % (midi_channel, hexify(state)))
midi.config.set_channel_state(midi_channel, state)
midi_remove_callback(midi_event_cb)
tulip.midi_remove_callback(midi_event_cb)

def run(screen):
screen.offset_y = 100
screen.quit_callback = quit
screen.set_bg_color(73)
screen.add([lfo, dco, hpf, vcf, vca, env, ch])
screen.add(midi_selector, relative=vcf, direction=lv.ALIGN.OUT_TOP_MID)
screen.add(patch_selector, relative=dco, direction=lv.ALIGN.OUT_TOP_MID)
screen.present()

midi_add_callback(midi_event_cb)
# Hook for communication from voices
screen.update_patch_for_channel_hook = update_patch_for_channel

tulip.midi_add_callback(midi_event_cb)
50 changes: 39 additions & 11 deletions tulip/shared/py/midi.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ def program_change(self, channel, patch):

def music_map(self, channel, patch_number=0, voice_count=None):
"""Implement the tulip music_map API."""
if not voice_count:
if (not voice_count
or (channel in self.synth_per_channel
and self.synth_per_channel[channel].num_voices == voice_count)):
# Simply changing patch.
self.program_change(channel, patch_number)
else:
Expand All @@ -56,12 +58,13 @@ def channel_info(self, channel):
def get_channel_state(self, channel):
if channel not in self.synth_per_channel:
return None
return self.synth_per_channel[channel].patch_state
return self.synth_per_channel[channel].get_patch_state()

def set_channel_state(self, channel, state):
if channel not in self.synth_per_channel:
raise ValueError('Attempting to set state for unallocated channel %d.' % channel)
self.synth_per_channel[channel].patch_state = state
#raise ValueError('Attempting to set state for unallocated channel %d.' % channel)
return
self.synth_per_channel[channel].set_patch_state(state)

def voices_for_channel(self, channel):
"""Return a list of AMY voices assigned to a channel."""
Expand Down Expand Up @@ -250,6 +253,10 @@ def __init__(self, voice_source, num_voices=6):
def amy_voices(self):
return [o.amy_voice for o in self.voice_objs]

@property
def num_voices(self):
return len(self.voice_objs)

def _get_next_voice(self):
"""Return the next voice to use."""
# First try free/released_voices in order, then steal from active_voices.
Expand Down Expand Up @@ -305,9 +312,18 @@ def sustain(self, state):
self.note_off(midinote)
self.sustained_notes = set()

def get_patch_state(self):
return self.patch_state

def set_patch_state(self, state):
self.patch_state = state

def program_change(self, patch_number):
self.patch_number = patch_number
self.voice_source.program_change(patch_number)
if patch_number != self.patch_number:
self.patch_number = patch_number
# Reset any modified state due to previous patch modifications.
self.patch_state = None
self.voice_source.program_change(self.patch_number)

def control_change(self, control, value):
self.voice_source.control_change(control, value)
Expand All @@ -317,10 +333,9 @@ def release_voices(self):
self.voice_source.release_voices()



class PitchedPCMSynth:
def __init__(self, num_voices=10):
self.oscs = list(range(amy.AMY_OSCS - num_voices, amy.AMY_OSCS))
self.oscs = list(range(amy.AMY_OSCS - num_voices, amy.AMY_OSCS)) # This will collide with Drums.
self.next_osc = 0
self.pcm_patch_to_osc = {}
# Fields used by UI
Expand All @@ -336,7 +351,7 @@ def note_on(self, note, velocity, pcm_patch, time=None):
patch=pcm_patch, vel=velocity)

def note_off(self, note, pcm_patch, time=None):
# Drums don't really need note-offs, but handle them anyway.
# Samples don't really need note-offs, but handle them anyway.
try:
osc = self.pcm_patch_to_osc[pcm_patch]
amy.send(time=time, osc=osc, vel=0)
Expand All @@ -345,7 +360,7 @@ def note_off(self, note, pcm_patch, time=None):
# We didn't recognize the patch; never mind.
pass

# Rest of Synth protocol doesn't do anything for drums.
# Rest of Synth protocol doesn't do anything for PitchedPCM.
def sustain(self, state):
pass

Expand All @@ -355,6 +370,11 @@ def program_change(self, patch_number):
def control_change(self, control, value):
pass

def get_patch_state(self):
return None

def set_patch_state(self, state):
pass


class DrumSynth:
Expand Down Expand Up @@ -397,6 +417,12 @@ def program_change(self, patch_number):
def control_change(self, control, value):
pass

def get_patch_state(self):
return None

def set_patch_state(self, state):
pass


def ensure_midi_config():
global config
Expand Down Expand Up @@ -453,7 +479,9 @@ def music_map(channel, patch_number=None, voice_count=None):
"""API to set a patch and polyphony for a given MIDI channel."""
config.music_map(channel, patch_number, voice_count)
try:
voices.refresh() # Update voices UI if it is running.
# Update voices UI if it is running.
voices_app = tulip.running_apps.get("voices", None)
#voices_app.refresh_with_new_music_map() # Not yet implemented!
except:
pass

Expand Down
Loading

0 comments on commit 4174588

Please sign in to comment.