-
Notifications
You must be signed in to change notification settings - Fork 0
/
JMMIDI_OSCBridge.sc
182 lines (166 loc) · 4.79 KB
/
JMMIDI_OSCBridge.sc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// this is turning into a bit of a god-object
// but I'm not going to refactor it today
JMMIDI_OSCBridge {
classvar <profiles;
var <patch, <func, <profile;
var <midi, midictl, controls, learning;
var aliveThread, addr;
*initClass {
profiles = IdentityDictionary[
\openstage -> {
var out = IdentityDictionary.new;
var spec = [0, 1].asSpec;
(1..24).do { |i|
var digits = if(i >= 10) { 2 } { 1 };
out.put(
("/fader_" ++ String.fill(2 - digits, $0) ++ i).asSymbol,
[i, spec]
); // oscpath --> [ccnum, inspec]
};
out
}.value
]
}
*new { |patch, profile(\openstage)|
^super.new.init(patch, profile)
}
init { |argPatch, argProfile|
patch = argPatch;
profile = profiles[argProfile];
if(profile.isNil) {
Error("Nonexistent OSC profile").throw;
};
if(patch.midi.isNil) { patch.initMidi };
midi = patch.midi;
// midi.cc() depends on the 'key', which is name++num
// this turns out to be inconvenient here
// so I make a collection to map the OSC path to the key
// (where the key becomes known at the time the parent adds a control)
// assuming that 'num' matches the ccnum defined in the profile
controls = IdentityDictionary.new;
learning = IdentityDictionary.new;
midictl = SimpleController(midi)
.put(\didFree, { this.free })
.put(\addCtl, { |obj, what, num, name, spec|
this.addCtl(num, name, spec)
})
.put(\removeCtl, { |obj, what, num, name|
this.removeCtl(num, name)
})
.put(\learning, { |obj, what, name, spec|
// note: if spec wasn't given, we want JMMIDI to look it up
// in the proxyspace.
// but a dictionary cannot contain nil
// so a nil spec needs a wrapper (Ref)
// ... we will unwrap later when processing the 'learning' entries
learning.put(name, `spec)
})
// .put(\learned, { |num, name|
// learning.clear;
// })
;
midi.midiFuncs.do { |midiResp|
if(#[noteOn, noteOff].includes(midiResp[\name]).not) {
this.addCtl(midiResp[\num], midiResp[\name], midiResp[\spec]);
};
};
func = { |msg, time, replyAddr|
var ctls = controls[msg[0]];
var spec = profile[msg[0]];
var value;
// this condition eliminates irrelevant incoming messages
// oscrecvfunc gets *everything*; ignore msgs we don't care about
if(spec.notNil) {
if(addr.isNil) { addr = replyAddr };
if(ctls.notNil) {
value = (spec[1].unmap(msg[1]) * 127.0).round.asInteger;
ctls.do { |key|
midi.cc(key, value)
};
};
learning.keysValuesDo { |name, controlSpec|
midi.addCtl(spec[0], name, controlSpec.dereference);
}; // otherwise ignore unknown (bc this function gets *everything*)
learning.clear;
};
};
thisProcess.addOSCRecvFunc(func);
}
free {
this.stopAliveThread;
thisProcess.removeOSCRecvFunc(func);
midictl.remove;
}
addCtl { |num, name, spec| // don't really need spec
var key = this.path(num, name);
if(key.isNil) {
"Unknown ccnum %".format(num).warn;
} {
if(controls[key].isNil) {
// maybe multiple parms are on the same ccnum
controls[key] = IdentitySet.new;
};
controls[key].add(midi.key(num, name))
};
this.startAliveThread;
}
removeCtl { |num, name|
var key = this.path(num, name);
if(key.isNil) {
"Unknown ccnum %".format(num).warn;
} {
if(controls[key].notNil) {
controls[key].remove(midi.key(num, name))
};
};
}
path { |num, name|
^profile.keys.detect { |key|
profile[key][0] == num
}
}
// the problem is: if you have multiple nodeproxies
// with the same arg name, the proxyspace doesn't sync
// their values. It doesn't broadcast changes either.
// So we have no guarantee of finding the one that changed latest,
// hence no guarantee of sending the right value to OSC.
// But a/ if there's only one proxy with that control, it's fine
// and b/ if you're controlling it from MIDI/OSC, just stick with that.
// JITLib synth args don't broadcast updates
// polling is the only way
startAliveThread {
if(aliveThread.isNil) {
aliveThread = SkipJack({
controls.keysValuesDo { |path, keys|
keys.do { |key|
var mfunc = midi.midiFuncs[key];
var name, value;
if(mfunc.notNil) {
name = mfunc[\name];
value = block { |break|
patch.proxyspace.keysValuesDo { |key, proxy|
if(proxy.nodeMap[name].isNumber) {
break.(proxy.nodeMap[name]);
};
};
nil
};
if(value.notNil) {
// note: profile spec is for OSC-side range, not OK here
value = mfunc[\spec].unmap(value);
if(addr.notNil) {
addr.sendMsg(path, value);
};
midi.changed(\cc, mfunc[\num], (value * 127.0).round.asInteger);
};
};
};
};
}, dt: 0.15, name: this.class.name.asString, clock: AppClock);
};
}
stopAliveThread {
aliveThread.stop;
aliveThread = nil
}
}