Skip to content

Commit

Permalink
Refining mapping process, added pinging process and output printing
Browse files Browse the repository at this point in the history
  • Loading branch information
loparcog committed Dec 1, 2024
1 parent 6f9ee7b commit df9afe6
Showing 1 changed file with 50 additions and 23 deletions.
73 changes: 50 additions & 23 deletions isobar/io/cv/output.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from ..output import OutputDevice
import numpy
import time

def get_cv_output_devices():
Expand All @@ -16,9 +17,12 @@ class CVOutputDevice(OutputDevice):

def audio_callback(self, out_data, frames, time, status):
for channel in range(self.channels):
value = self.channel_notes[channel]
value = self.channel_cvs[channel]
if value is None:
value = 0.0
if self.ping_flag[channel]:
value = 10.0
self.ping_flag[channel] = False
out_data[:, channel] = value

def __init__(self, device_name=None, sample_rate=44100):
Expand Down Expand Up @@ -58,14 +62,20 @@ def __init__(self, device_name=None, sample_rate=44100):

# Expert Sleepers ES-8 supports entire -10V to +10V range
self.output_voltage_max = 10
# Number of channels available
self.channels = self.stream.channels
self.channel_notes = [None] * self.channels
self.channel_map = [None] * self.channels
# Channel output CV values
self.channel_cvs = [None] * self.channels
# Channel MIDI to CV mappings
self.channel_map = {}
# Ping flag
self.ping_flag = [False] * self.channels

print("Started CV output with %d channels" % self.channels)

# TODO: Retrigger event possible?
def set_channels(self, midi_channel=0, note_channel=None, velocity_channel=None, gate_channel=None):
# TODO: Add polyphony handling and settings
def map_channels(self, midi_channel=0, note_channel=None, velocity_channel=None, gate_channel=None):
"""
Distribute CV outputs from a single MIDI channel.
Expand All @@ -77,8 +87,8 @@ def set_channels(self, midi_channel=0, note_channel=None, velocity_channel=None,
velocity_channel (int): CV channel to output note velocity
gate_channel (int): CV channel to output current gate (10V when open)
"""
if not all((ch == None or ch >= 0) for ch in [midi_channel, note_channel, velocity_channel, gate_channel]):
print("set_channels: All set channels need to be an integer greater than 0")
if not all((ch == None or (ch >= 0 and ch <= self.channels)) for ch in [midi_channel, note_channel, velocity_channel, gate_channel]):
print("set_channels: All set channels need to be an integer greater than 0 and less than the channel max (%d)" % self.channels)

# Mappings in a list of [note, velocity, gate]
self.channel_map[midi_channel] = [note_channel, velocity_channel, gate_channel]
Expand All @@ -92,14 +102,29 @@ def reset_channel(self, midi_channel):
Args:
midi_channel (int): MIDI channel to erase CV channel pairings from
"""
self.channel_map[midi_channel] = None
# Set all outputs to 0
mappings = self.channel_map.get(midi_channel)
if (mappings):
for ch in self.channel_map[midi_channel]:
self._set_channel_value(ch, None)
del self.channel_map[midi_channel]
print("MIDI channel %d mappings removed" % midi_channel)
else:
print("No MIDI channel %d mappings found" % midi_channel)

def show_channels(self):
"""
Show all currently assigned channels.
Display all channels that are currently assigned in a tree view
"""
# Loop through dictionary for outputs
for midi_channel in self.channel_map:
# Print MIDI title
print("MIDI Channel %d" % midi_channel)
print("\t\\Note: ch%d" % midi_channel[0])
print("\t\\Velocity: ch%d" % midi_channel[0])
print("\t\\Gate: ch%d" % midi_channel[0])

def ping_channel(self, channel):
"""
Expand All @@ -110,48 +135,50 @@ def ping_channel(self, channel):
Args:
channel (int): CV channel to output ping
"""
self.note_on(channel=channel)
time.sleep(0.1)
self.note_off(channel=channel)
self.ping_flag[channel] = True

# TODO: Implement bipolar setting
def _note_index_to_amplitude(self, note, bipolar=False):
# Reduce to -5V to 5V if bipolar
note_float = (note / (12 * self.output_voltage_max)) - (0.5 * bipolar)
if note_float < -1.0 or note_float > 1.0:
raise ValueError("Note index %d is outside the voltage range supported by this device" % note)
print("note %d, float %f" % (note, note_float))
print(12 * self.output_voltage_max)
return note_float

def _set_channel_value(self, channel, cv):
if cv and (cv < -1.0 or cv > 1.0):
raise ValueError("CV value %f is outside the voltage range supported by this device" % cv)
self.channel_cvs[channel] = cv

def note_on(self, note=60, velocity=64, channel=None):
note_float = self._note_index_to_amplitude(note)
print("Note On: %d, CV %f" % (note, note_float))
# See if the specified MIDI channel exists
channel_set = self.channel_map[channel]
if (channel_set is not None):
channel_set = self.channel_map.get(channel)
if (channel_set):
# Distribute outputs (note, velocity, gate)
output_cvs = [note_float, (velocity/127), 1.0]
for i in range(len(channel_set)):
self.channel_notes[channel_set[i]] = output_cvs[i]
for ch, cv in zip(channel_set, output_cvs):
self._set_channel_value(ch, cv)
# Otherwise select the next open channel
else:
for index, channel_note in enumerate(self.channel_notes):
for index, channel_note in enumerate(self.channel_cvs):
if channel_note is None:
self.channel_notes[index] = note_float
self._set_channel_value(index, None)
break

def note_off(self, note=60, channel=None):
# See if the specified MIDI channel exists
channel_set = self.channel_map[channel]
channel_set = self.channel_map.get(channel)
if (channel_set is not None):
# Turn all outputs off
for i in range(len(channel_set)):
self.channel_notes[channel_set[i]] = 0
for ch in channel_set:
self._set_channel_value(ch, 0)
# Otherwise select the next open channel
note_float = self._note_index_to_amplitude(note)
for index, channel_note in enumerate(self.channel_notes):
for index, channel_note in enumerate(self.channel_cvs):
if channel_note is not None and channel_note == note_float:
self.channel_notes[index] = None
self._set_channel_value(index, None)

def control(self, control, value, channel=0):
pass

0 comments on commit df9afe6

Please sign in to comment.