Skip to content

Commit

Permalink
added arpegg.py, working with voices.py (for latest chan).
Browse files Browse the repository at this point in the history
  • Loading branch information
dpwe committed Apr 16, 2024
1 parent f58cc08 commit 9f00fcc
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 52 deletions.
47 changes: 23 additions & 24 deletions tulip/fs/app/voices/voices.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,24 +87,21 @@ def __init__(self, width=350, height=300):
self.range.group.set_style_bg_color(tulip.pal_to_lv(9),0)
self.range.group.align_to(self.mode.group, lv.ALIGN.OUT_RIGHT_TOP, 10, 0)

def mode_cb(self,e):
button = e.get_target_obj()
midi.arpegg_mode = button.get_index()
def tempo_cb(self,e):
new_bpm = self.tempo.get_value()*2.4
if(new_bpm < 1.0): new_bpm = 1
tulip.seq_bpm(new_bpm)
self.tempo_label.set_text("%d BPM" % (tulip.seq_bpm()))
def hold_cb(self,e):
if(self.hold.get_state()==3):
midi.arpegg_hold = True
midi.arpeggiator.set('hold', True)
else:
midi.arpegg_hold = False
midi.arpeggiator.set('hold', False)
def arpegg_cb(self,e):
if(self.arpegg.get_state()==3):
midi.arpegg = True
if(self.arpegg.get_state()==3):
midi.arpeggiator.set('on', True)
else:
midi.arpegg = False
midi.arpeggiator.set('on', False)


class ListColumn(tulip.UIElement):
Expand Down Expand Up @@ -146,13 +143,14 @@ def select(self, index, defer=False):
if index is not None:
self.buttons[self.selected].set_style_bg_color(tulip.pal_to_lv(129), 0)
if(self.name=='channel'):
current_patch(self.selected+1)
current_patch(self.selected + 1)
elif(self.name=='synth'):
update_patches(self.button_texts[self.selected])
elif(self.name=='mode'):
midi.arpegg_mode = index
mode = ['up', 'down', 'updown', 'rand'][index]
midi.arpeggiator.set('direction', mode)
elif(self.name=='range'):
midi.arpegg_range = index+1
midi.arpeggiator.set('octaves', index + 1)
else:
if not defer: update_map()

Expand Down Expand Up @@ -219,8 +217,9 @@ def update_map():
channel = app.channels.selected + 1
polyphony = app.polyphony.selected + 1
# Check if this is a new thing
if not (midi.patch_map.get(channel, None) == patch_no and midi.polyphony_map.get(channel, None) == polyphony):
tulip.music_map(channel, patch_number=patch_no, voice_count=polyphony)
if midi.config.channel_info(channel) != (patch_no, polyphony):
tulip.music_map(channel, patch_number=patch_no,
voice_count=polyphony)

# populate the patches dialog from patches,oy
def update_patches(synth):
Expand All @@ -235,25 +234,25 @@ def update_patches(synth):
app.patches.replace_items([])
app.patches.label.set_text("%s patches" % (synth))

# Get current settings for a channel from midi.patch_map
# Get current settings for a channel from midi.config.
def current_patch(channel):
global app
if(channel in midi.patch_map):
p = midi.patch_map[channel]
if(p<128):
patch_num, polyphony = midi.config.channel_info(channel)
if patch_num is not None:
if patch_num < 128:
# We defer here so that setting the UI component doesn't trigger an update before it updates
app.synths.select(0, defer=True)
app.patches.select(p, defer=True)
elif(p>128 and p<256):
app.patches.select(patch_num, defer=True)
elif patch_num > 128 and patch_num < 256:
app.synths.select(1, defer=True)
app.patches.select(p-128, defer=True)
elif(p<1024):
app.patches.select(patch_num - 128, defer=True)
elif patch_num < 1024:
app.synths.select(2, defer=True)
app.patches.select(p-256, defer=True)
app.patches.select(patch_num - 256, defer=True)
else:
app.synths.select(3, defer=True)
app.patches.select(p-1024, defer=True)
app.polyphony.select(midi.polyphony_map[channel]-1, defer=True)
app.patches.select(patch_num - 1024, defer=True)
app.polyphony.select(polyphony - 1, defer=True)
else:
# no patch set for this chanel
app.patches.select(None)
Expand Down
165 changes: 165 additions & 0 deletions tulip/shared/py/arpegg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""Arpeggiator for midi input."""

import time
import random

class ArpeggiatorSynth:
"""Create arpeggios."""
# State
synth = None # Downstream synthesizer object.
current_active_notes = None # Set of notes currently down on keyboard.
arpeggiate_base_notes = None # Set of notes currently driving the arpeggio.
full_sequence = None # List of notes in arpeggio (including direction and octave).
current_note = None # Last note sent to synth (i.e., next note-off).
current_step = -1 # Current position in sequence.
running = False # Currently mid-sequence. Goes false if no notes are playing.
# UI control items
active = False
hold = False
octaves = 1
direction = "up"
period_ms = 125
# Velocity for all the notes generated by the sequencer.
velocity = 0.5
# Notes at or above the split_note are always passed through live, not sequenced.
split_note = 128 # Split is off the end of the keyboard, i.e., inactive.

def __init__(self, synth, channel=0):
self.synth = synth
self.channel = channel # This is just bookkeeping for my owner, not used.
self.current_active_notes = set()
self.arpeggiate_base_notes = set()
self.full_sequence = []

def note_on(self, note, vel):
if not self.active or note >= self.split_note:
return self.synth.note_on(note, vel)
if self.hold and not self.current_active_notes:
# First note after all keys off resets hold set.
self.arpeggiate_base_notes = set()
# Adding keys to some already down.
self.current_active_notes.add(note)
# Because it's a set, can't get more than one instance of a base note.
self.arpeggiate_base_notes.add(note)
self._update_full_sequence()

def note_off(self, note):
if not self.active or note >= self.split_note:
return self.synth.note_off(note)
#print(self.current_active_notes, self.arpeggiate_base_notes)
# Update our internal record of keys currently held down.
self.current_active_notes.remove(note)
if not self.hold:
# If not hold, remove notes from active set when released.
self.arpeggiate_base_notes.remove(note)
self._update_full_sequence()

def _update_full_sequence(self):
"""The full note loop given base_notes, octaves, and direction."""
# Basic notes, ascending.
basic_notes = sorted(self.arpeggiate_base_notes)
# Apply octaves
notes = []
for o in range(self.octaves):
notes = notes + [n + 12 * o for n in basic_notes]
# Apply direction
if self.direction == "down":
notes = notes[::-1]
elif self.direction == "updown":
notes = notes + notes[-2:0:-1]
self.full_sequence = notes
if self.full_sequence and not self.running:
# Prepare to start a new sequence at the first note.
self.current_step = -1
# Semaphore to the run loop to start going.
self.running = True

def next_note(self):
if self.current_note:
self.synth.note_off(self.current_note)
self.current_note = None
if self.full_sequence:
if self.direction == "rand":
self.current_step = random.randint(0, len(self.full_sequence) - 1)
else:
self.current_step = (self.current_step + 1) % len(self.full_sequence)
self.current_note = self.full_sequence[self.current_step]
self.synth.note_on(self.current_note, self.velocity)
else:
self.running = False

def run(self):
# Endless function that will emit sequencer notes when there are arpeggiate_base_notes.
while True:
if not self.running:
time.sleep_ms(10) # Break up the loop a little
else:
# self.running started sequence.
# Another brief pause to let all keys go down
time.sleep_ms(10)
# Cycle the notes as long as we have them.
while self.running:
self.next_note()
time.sleep_ms(self.period_ms)

def control_change(self, control, value):
#if not self.active:
# return self.synth.control_change(control, value)
if control == self.rate_control_num:
self.period_ms = 25 + 5 * value # 25 to 665 ms
elif control == self.octaves_control_num:
self.cycle_octaves()
elif control == self.direction_control_num:
self.cycle_direction()
else:
self.synth.control_change(control, value)
self._update_full_sequence()

def program_change(self, patch_number):
self.synth.program_change(patch_number)

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

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

def _cycle_octaves(self):
self.octaves = 1 + (self.octaves % 3)

def _cycle_direction(self):
if self.direction == 'up':
self.direction = 'down'
elif self.direction == 'down':
self.direction = 'updown'
elif self.direction == 'updown':
self.direction = 'rand'
else:
self.direction = 'up'

def set(self, arg, val=None):
"""Callback for external control."""
#print("arp set", arg, val)
#if self.active:
# return self.synth.set(arg, val)
if arg == 'on':
self.active = val
# Reset hold state when on/off changes.
self.arpeggiate_base_notes = set()
elif arg == 'hold':
self.hold = val
# Copy across the current_active_notes after a change in hold.
self.arpeggiate_base_notes = set(self.current_active_notes)
elif arg == 'arp_rate':
self.period_ms = int(1000 / (2.0 ** (5 * val))) # 1 Hz to 32 Hz
elif arg == 'octaves':
self.octaves = val
elif arg == 'direction':
self.direction = val
self._update_full_sequence()

def get_new_voices(self, num_voices):
return self.synth.get_new_voices(num_voices)

Loading

0 comments on commit 9f00fcc

Please sign in to comment.