Skip to content

Commit

Permalink
voices.py,midi.py: Able to change polyphony without running out of vo…
Browse files Browse the repository at this point in the history
…ices.
  • Loading branch information
dpwe committed Apr 17, 2024
1 parent aab72bd commit d92b61b
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 39 deletions.
10 changes: 5 additions & 5 deletions tulip/fs/app/juno6/juno6.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,11 +304,11 @@ def set_value(self, value):
juno_patch_for_midi_channel = {}

# Get the patch to channel mapping and the AMY voices from midi.patch_map if set
for i in range(16):
channel = i+1
if channel in midi.patch_map and midi.patch_map[channel] < 128:
jp = juno.JunoPatch.from_patch_number(midi.patch_map[channel])
jp.voices = midi.voices_for_channel[channel]
for channel in range(1, 17):
patch_num, amy_voices = midi.config.channel_info(channel)
if patch_num is not None and patch_num < 128:
jp = juno.JunoPatch.from_patch_number(patch_num)
jp.set_voices(amy_voices)
juno_patch_for_midi_channel[channel] = jp

def current_juno():
Expand Down
26 changes: 15 additions & 11 deletions tulip/fs/app/voices/voices.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ def select(self, index, defer=False):
elif(self.name=='range'):
midi.arpeggiator.set('octaves', index + 1)
else:
if not defer: update_map()
if not defer:
update_map()

def list_cb(self, e):
button = e.get_target_obj()
Expand Down Expand Up @@ -217,7 +218,9 @@ def update_map():
channel = app.channels.selected + 1
polyphony = app.polyphony.selected + 1
# Check if this is a new thing
if midi.config.channel_info(channel) != (patch_no, polyphony):
channel_patch, amy_voices = midi.config.channel_info(channel)
channel_polyphony = 0 if amy_voices is None else len(amy_voices)
if (channel_patch, channel_polyphony) != (patch_no, polyphony):
tulip.music_map(channel, patch_number=patch_no,
voice_count=polyphony)

Expand All @@ -237,21 +240,22 @@ def update_patches(synth):
# Get current settings for a channel from midi.config.
def current_patch(channel):
global app
patch_num, polyphony = midi.config.channel_info(channel)
if patch_num is not None:
if patch_num < 128:
channel_patch, amy_voices = midi.config.channel_info(channel)
if channel_patch is not None:
polyphony = len(amy_voices)
if channel_patch < 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(patch_num, defer=True)
elif patch_num > 128 and patch_num < 256:
app.patches.select(channel_patch, defer=True)
elif channel_patch < 256:
app.synths.select(1, defer=True)
app.patches.select(patch_num - 128, defer=True)
elif patch_num < 1024:
app.patches.select(channel_patch - 128, defer=True)
elif channel_patch < 1024:
app.synths.select(2, defer=True)
app.patches.select(patch_num - 256, defer=True)
app.patches.select(channel_patch - 256, defer=True)
else:
app.synths.select(3, defer=True)
app.patches.select(patch_num - 1024, defer=True)
app.patches.select(channel_patch - 1024, defer=True)
app.polyphony.select(polyphony - 1, defer=True)
else:
# no patch set for this chanel
Expand Down
7 changes: 5 additions & 2 deletions tulip/shared/py/arpegg.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ def program_change(self, patch_number):
return self.synth.program_change(patch_number)

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

@property
def patch_number(self):
Expand Down Expand Up @@ -136,3 +136,6 @@ def set(self, arg, val=None):
def get_new_voices(self, num_voices):
return self.synth.get_new_voices(num_voices)

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

76 changes: 55 additions & 21 deletions tulip/shared/py/midi.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ class MidiConfig:
"""System-wide Midi input config."""

def __init__(self, voices_per_channel, patch_per_channel):
self.synth_per_channel = [None] * 16 # Fixed array of 16 slots for Synths.
self.synth_per_channel = dict()
for channel, polyphony in voices_per_channel.items():
patch = patch_per_channel[channel] if channel in patch_per_channel else None
self.add_synth(channel, patch, polyphony)

def add_synth(self, channel, patch, polyphony):
if channel in self.synth_per_channel:
# Old Synth allocated - Expicitly return the amy_voices to the pool.
release_arpeggiator(channel)
self.synth_per_channel[channel].release_voices()
del self.synth_per_channel[channel]
if channel == 10:
synth = DrumSynth(num_voices=polyphony)
else:
Expand All @@ -40,13 +45,19 @@ def music_map(self, channel, patch_number=0, voice_count=None):
self.add_synth(channel, patch_number, voice_count)

def channel_info(self, channel):
"""Report the current patch_num and polyphony for this channel."""
"""Report the current patch_num and list of amy_voices for this channel."""
if channel not in self.synth_per_channel:
return (None, None)
return (
self.synth_per_channel[channel].patch_number,
self.synth_per_channel[channel].num_voices,
self.synth_per_channel[channel].amy_voices,
)

def voices_for_channel(self, channel):
"""Return a list of AMY voices assigned to a channel."""
if channel not in self.synth_per_channel:
return []
return self.synth_per_channel[channel].amy_voices()



Expand Down Expand Up @@ -147,14 +158,18 @@ def note_off(self, time=None):
class VoiceSource:
"""Manage the pool of amy voices. Provide voice_source for Synth objects."""
# Class-wide record of which voice to allocate next.
next_amy_voice = 0
allocated_amy_voices = set()

def get_new_voices(self, num_voices):
self.amy_voice_nums = list(
range(VoiceSource.next_amy_voice,
VoiceSource.next_amy_voice + num_voices)
)
VoiceSource.next_amy_voice += num_voices
new_voices = []
next_amy_voice = 0
while len(new_voices) < num_voices:
while next_amy_voice in VoiceSource.allocated_amy_voices:
next_amy_voice += 1
new_voices.append(next_amy_voice)
next_amy_voice += 1
self.amy_voice_nums = new_voices
VoiceSource.allocated_amy_voices.update(new_voices)
voice_objects = []
for amy_voice_num in self.amy_voice_nums:
voice_objects.append(VoiceObject(amy_voice_num))
Expand All @@ -170,6 +185,11 @@ def control_change(self, control, value):
print('control_change not implemented for amy-managed voices.')
pass

def release_voices(self):
"""Return the amy_voices when the VoiceSource is no longer needed."""
for amy_voice in self.amy_voice_nums:
VoiceSource.allocated_amy_voices.remove(amy_voice)


class Synth:
"""Manage a polyphonic synthesizer by rotating among a fixed pool of voices.
Expand All @@ -180,7 +200,7 @@ class Synth:
synth.control_change(control, value)
synth.program_change(patch_num)
Provides read-back attributes (for voices.py UI):
synth.num_voices
synth.amy_voices
synth.patch_number
Argument voice_source provides the following methods:
Expand Down Expand Up @@ -211,9 +231,13 @@ def __init__(self, voice_source, num_voices=6):
self.sustaining = False
self.sustained_notes = set()
# Fields used by UI
self.num_voices = num_voices
#self.num_voices = num_voices
self.patch_number = None

@property
def amy_voices(self):
return [o.amy_voice for o in 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 @@ -276,17 +300,21 @@ def program_change(self, patch_number):
def control_change(self, control, value):
self.voice_source.control_change(control, value)

def release_voices(self):
# Make sure the voice source is deleted, so all the amy_voices get returned.
self.voice_source.release_voices()


class DrumSynth:
"""Simplified Synth for Drum channel (10)."""
PCM_PATCHES = 29

def __init__(self, num_voices=10):
self.oscs = range(amy.AMY_OSCS - num_voices, amy.AMY_OSCS)
self.oscs = list(range(amy.AMY_OSCS - num_voices, amy.AMY_OSCS))
self.next_osc = 0
self.note_to_osc = {}
# Fields used by UI
self.num_voices = num_voices
self.amy_voices = self.oscs # Actually osc numbers not amy voices.
self.patch_number = 0

def note_on(self, note, velocity, time=None):
Expand Down Expand Up @@ -337,13 +365,13 @@ def midi_event_cb(x):
while m is not None and len(m) > 0:
message = m[0] & 0xF0
channel = (m[0] & 0x0F) + 1
set_arpegg_chan(channel)
synth = config.synth_per_channel[channel]
if not synth:
insert_arpeggiator(channel)
if channel not in config.synth_per_channel:
if channel not in WARNED_MISSING_CHANNELS:
print("Warning: No synth configured for MIDI channel", channel)
WARNED_MISSING_CHANNELS.add(channel)
else:
synth = config.synth_per_channel[channel]
if message == 0x90: # Note on.
midinote = m[1]
midivel = m[2]
Expand Down Expand Up @@ -379,15 +407,21 @@ def music_map(channel, patch_number=None, voice_count=None):

arpeggiator = arpegg.ArpeggiatorSynth(synth=None, channel=0)

def set_arpegg_chan(channel):
if arpeggiator.channel != channel and config.synth_per_channel[channel]:
if arpeggiator.channel != 0:
# Uninstall arpeggiator from previous channel.
config.synth_per_channel[arpeggiator.channel] = arpeggiator.synth
def insert_arpeggiator(channel):
if arpeggiator.channel != channel and channel in config.synth_per_channel:
release_arpeggiator()
arpeggiator.synth = config.synth_per_channel[channel]
arpeggiator.channel = channel
config.synth_per_channel[channel] = arpeggiator

def release_arpeggiator(channel=None):
"""De-insert arpeggiator in front of current channel's synth, e.g. before changing synth."""
# If channel is provided, only release the arpeggiator if it's on this channel.
if channel is None or arpeggiator.channel == channel:
if arpeggiator.channel:
config.synth_per_channel[arpeggiator.channel] = arpeggiator.synth
arpeggiator.channel = 0

def midi_step(time):
if(tulip.seq_ticks() > tulip.seq_ppq()):
ensure_midi_config()
Expand Down

0 comments on commit d92b61b

Please sign in to comment.