1
1
from ..output import OutputDevice
2
+ import time
2
3
3
4
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)" )
4
10
return list (sounddevice .query_devices ())
5
11
6
12
class CVOutputDevice (OutputDevice ):
@@ -20,7 +26,7 @@ def __init__(self, device_name=None, sample_rate=44100):
20
26
Create a control voltage output device.
21
27
22
28
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.
24
30
25
31
Args:
26
32
device_name (str): Name of the audio output device to use.
@@ -30,14 +36,15 @@ def __init__(self, device_name=None, sample_rate=44100):
30
36
super ().__init__ ()
31
37
32
38
#--------------------------------------------------------------------------------
33
- # Lazily import sounddevice, to avoid the additional time cost of initializing
39
+ # Lazily import sounddevice, to avoid the additional time cost of initializing
34
40
# PortAudio when not needed
35
41
#--------------------------------------------------------------------------------
36
42
try :
37
43
import sounddevice
38
44
import numpy as np
39
45
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)" )
41
48
42
49
try :
43
50
self .stream = sounddevice .OutputStream (device = device_name ,
@@ -53,29 +60,98 @@ def __init__(self, device_name=None, sample_rate=44100):
53
60
self .output_voltage_max = 10
54
61
self .channels = self .stream .channels
55
62
self .channel_notes = [None ] * self .channels
56
- self .midi_c0 = 0
63
+ self .channel_map = [ None ] * self . channels
57
64
58
65
print ("Started CV output with %d channels" % self .channels )
59
66
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 )
62
121
if note_float < - 1.0 or note_float > 1.0 :
63
122
raise ValueError ("Note index %d is outside the voltage range supported by this device" % note )
64
123
print ("note %d, float %f" % (note , note_float ))
124
+ print (12 * self .output_voltage_max )
65
125
return note_float
66
126
67
- def note_on (self , note = 60 , velocity = 64 , channel = 0 ):
127
+ def note_on (self , note = 60 , velocity = 64 , channel = None ):
68
128
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
73
142
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
75
151
note_float = self ._note_index_to_amplitude (note )
76
152
for index , channel_note in enumerate (self .channel_notes ):
77
153
if channel_note is not None and channel_note == note_float :
78
154
self .channel_notes [index ] = None
79
155
80
156
def control (self , control , value , channel = 0 ):
81
- pass
157
+ pass
0 commit comments