Skip to content

Commit 6f9ee7b

Browse files
committed
CV channel and additional output class functionality
1 parent 4f4a59a commit 6f9ee7b

File tree

1 file changed

+89
-13
lines changed

1 file changed

+89
-13
lines changed

isobar/io/cv/output.py

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
from ..output import OutputDevice
2+
import time
23

34
def get_cv_output_devices():
5+
try:
6+
import sounddevice
7+
except ModuleNotFoundError:
8+
raise RuntimeError(
9+
"get_cv_output_devices: Couldn't import the sounddevice module (to install: pip3 install sounddevice)")
410
return list(sounddevice.query_devices())
511

612
class CVOutputDevice(OutputDevice):
@@ -20,7 +26,7 @@ def __init__(self, device_name=None, sample_rate=44100):
2026
Create a control voltage output device.
2127
2228
Control voltage signals require a DC-coupled audio interface, which Python
23-
sends audio signals to via the sounddevice library.
29+
sends audio signals to via the sounddevice library.
2430
2531
Args:
2632
device_name (str): Name of the audio output device to use.
@@ -30,14 +36,15 @@ def __init__(self, device_name=None, sample_rate=44100):
3036
super().__init__()
3137

3238
#--------------------------------------------------------------------------------
33-
# Lazily import sounddevice, to avoid the additional time cost of initializing
39+
# Lazily import sounddevice, to avoid the additional time cost of initializing
3440
# PortAudio when not needed
3541
#--------------------------------------------------------------------------------
3642
try:
3743
import sounddevice
3844
import numpy as np
3945
except ModuleNotFoundError:
40-
raise RuntimeError("CVOutputDevice: Couldn't import the sounddevice or numpy modules (to install: pip3 install sounddevice numpy)")
46+
raise RuntimeError(
47+
"CVOutputDevice: Couldn't import the sounddevice or numpy modules (to install: pip3 install sounddevice numpy)")
4148

4249
try:
4350
self.stream = sounddevice.OutputStream(device=device_name,
@@ -53,29 +60,98 @@ def __init__(self, device_name=None, sample_rate=44100):
5360
self.output_voltage_max = 10
5461
self.channels = self.stream.channels
5562
self.channel_notes = [None] * self.channels
56-
self.midi_c0 = 0
63+
self.channel_map = [None] * self.channels
5764

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

60-
def _note_index_to_amplitude(self, note):
61-
note_float = (note - self.midi_c0) / (12 * self.output_voltage_max)
67+
# TODO: Retrigger event possible?
68+
def set_channels(self, midi_channel=0, note_channel=None, velocity_channel=None, gate_channel=None):
69+
"""
70+
Distribute CV outputs from a single MIDI channel.
71+
72+
Select what channels to output CV data from a number of MIDI properties.
73+
74+
Args:
75+
midi_channel (int): MIDI channel to receive data from, commonly set in pattern scheduling
76+
note_channel (int): CV channel to output note using V/OCT frequency
77+
velocity_channel (int): CV channel to output note velocity
78+
gate_channel (int): CV channel to output current gate (10V when open)
79+
"""
80+
if not all((ch == None or ch >= 0) for ch in [midi_channel, note_channel, velocity_channel, gate_channel]):
81+
print("set_channels: All set channels need to be an integer greater than 0")
82+
83+
# Mappings in a list of [note, velocity, gate]
84+
self.channel_map[midi_channel] = [note_channel, velocity_channel, gate_channel]
85+
86+
def reset_channel(self, midi_channel):
87+
"""
88+
Reset all CV channel mappings for a given MIDI channel.
89+
90+
Remove any CV outputs for the given MIDI channel
91+
92+
Args:
93+
midi_channel (int): MIDI channel to erase CV channel pairings from
94+
"""
95+
self.channel_map[midi_channel] = None
96+
97+
def show_channels(self):
98+
"""
99+
Show all currently assigned channels.
100+
101+
Display all channels that are currently assigned in a tree view
102+
"""
103+
104+
def ping_channel(self, channel):
105+
"""
106+
Ping an output CV channel to test connection
107+
108+
Send a 5V signal to a specified channel for 100ms
109+
110+
Args:
111+
channel (int): CV channel to output ping
112+
"""
113+
self.note_on(channel=channel)
114+
time.sleep(0.1)
115+
self.note_off(channel=channel)
116+
117+
# TODO: Implement bipolar setting
118+
def _note_index_to_amplitude(self, note, bipolar=False):
119+
# Reduce to -5V to 5V if bipolar
120+
note_float = (note / (12 * self.output_voltage_max)) - (0.5 * bipolar)
62121
if note_float < -1.0 or note_float > 1.0:
63122
raise ValueError("Note index %d is outside the voltage range supported by this device" % note)
64123
print("note %d, float %f" % (note, note_float))
124+
print(12 * self.output_voltage_max)
65125
return note_float
66126

67-
def note_on(self, note=60, velocity=64, channel=0):
127+
def note_on(self, note=60, velocity=64, channel=None):
68128
note_float = self._note_index_to_amplitude(note)
69-
for index, channel_note in enumerate(self.channel_notes):
70-
if channel_note is None:
71-
self.channel_notes[index] = note_float
72-
break
129+
# See if the specified MIDI channel exists
130+
channel_set = self.channel_map[channel]
131+
if (channel_set is not None):
132+
# Distribute outputs (note, velocity, gate)
133+
output_cvs = [note_float, (velocity/127), 1.0]
134+
for i in range(len(channel_set)):
135+
self.channel_notes[channel_set[i]] = output_cvs[i]
136+
# Otherwise select the next open channel
137+
else:
138+
for index, channel_note in enumerate(self.channel_notes):
139+
if channel_note is None:
140+
self.channel_notes[index] = note_float
141+
break
73142

74-
def note_off(self, note=60, channel=0):
143+
def note_off(self, note=60, channel=None):
144+
# See if the specified MIDI channel exists
145+
channel_set = self.channel_map[channel]
146+
if (channel_set is not None):
147+
# Turn all outputs off
148+
for i in range(len(channel_set)):
149+
self.channel_notes[channel_set[i]] = 0
150+
# Otherwise select the next open channel
75151
note_float = self._note_index_to_amplitude(note)
76152
for index, channel_note in enumerate(self.channel_notes):
77153
if channel_note is not None and channel_note == note_float:
78154
self.channel_notes[index] = None
79155

80156
def control(self, control, value, channel=0):
81-
pass
157+
pass

0 commit comments

Comments
 (0)