|
| 1 | +( |
| 2 | +thisProcess.openUDPPort(6789); // from tidal |
| 3 | +~python = NetAddr.new("127.0.0.1", 9999); // to python |
| 4 | +) |
| 5 | + |
| 6 | + |
| 7 | +// for sending MIDI to ableton |
| 8 | +( |
| 9 | +MIDIClient.init; |
| 10 | +// MIDIClient.destinations |
| 11 | +~m1 = MIDIOut.newByName("IAC Driver", "Bus 1").latency_(0); |
| 12 | +~m2 = MIDIOut.newByName("IAC Driver", "Bus 2").latency_(0); |
| 13 | +~m3 = MIDIOut.newByName("IAC Driver", "Bus 3").latency_(0); |
| 14 | +~prog2portchan = { arg prog; |
| 15 | + case |
| 16 | + {prog==0}{"can't convert start token".postln} |
| 17 | + {prog<=128}{ |
| 18 | + var group = (prog-1 /8).asInteger; |
| 19 | + var idx = (prog-1 %8).asInteger; |
| 20 | + var port = switch(group) |
| 21 | + { 0}{case |
| 22 | + {idx<4}{~m1} //acoustic |
| 23 | + {idx<6}{~m2} //electric |
| 24 | + {true}{~m3} //harpsichord |
| 25 | + } //piano |
| 26 | + { 1}{((idx<3)||(idx==5)).if{~m1}{~m2}} //chromatic perc |
| 27 | + { 2}{(idx<4).if{~m1}{~m2}} //organ |
| 28 | + { 3}{(idx<5).if{~m1}{~m2}} //guitar |
| 29 | + { 4}{(idx<4).if{~m2}{~m1}} //bass |
| 30 | + { 5}{(idx<5).if{~m1}{~m2}} //strings |
| 31 | + { 6}{(idx<4).if{~m1}{~m2}} //ensemble |
| 32 | + { 7}{(idx<3).if{~m1}{~m2}} //brass |
| 33 | + { 8}{(idx<4).if{~m1}{~m2}} //reed |
| 34 | + { 9}{(idx<3).if{~m1}{~m2}} //pipe |
| 35 | + {10}{(idx%2==0).if{~m1}{~m2}} //synth lead |
| 36 | + {11}{(idx<4).if{~m1}{~m2}} //synth pad |
| 37 | + {12}{~m1} //synth fx |
| 38 | + {13}{case //'ethnic' |
| 39 | + {idx<=3}{~m1} |
| 40 | + {idx==4}{~m2} |
| 41 | + {idx==5}{group=2;~m2} |
| 42 | + {idx==6}{group=5;~m1} |
| 43 | + {idx==7}{group=8;~m2} |
| 44 | + } |
| 45 | + {14}{~m1} //percussive |
| 46 | + {15}{~m1} //sound fx |
| 47 | + ; |
| 48 | + // \melody.postln; |
| 49 | + (port:port, chan:group) |
| 50 | + }{(prog<=256)||(prog>=265)}{ |
| 51 | + // \drum.postln; |
| 52 | + (port:~m2, chan:12) |
| 53 | + }{prog-257 < 8}{ |
| 54 | + // \anon.postln; |
| 55 | + (port:~m1, chan:0) |
| 56 | + } |
| 57 | +}; |
| 58 | +~release_all = {arg vel=0; |
| 59 | + [~m1, ~m2, ~m3].do{arg port; 128.do{arg note; 16.do{arg chan; |
| 60 | + port.noteOff(chan, note, vel)}}} |
| 61 | +}; |
| 62 | +~midi_send = {arg inst, pitch, vel; |
| 63 | + var port_chan = ~prog2portchan.(inst); |
| 64 | + var port = port_chan[\port]; |
| 65 | + var chan = port_chan[\chan]; |
| 66 | + (vel>0).if{ |
| 67 | + port.noteOn(chan, pitch, vel.max(1)); |
| 68 | + }{ |
| 69 | + port.noteOff(chan, pitch); |
| 70 | + } |
| 71 | +} |
| 72 | +) |
| 73 | + |
| 74 | +OSCdef.trace(false); |
| 75 | + |
| 76 | + |
| 77 | +// ok so how to actually 'schedule' things? |
| 78 | +// SystemClock one-offs should do? |
| 79 | +// schedule notochord-processing of each event for time of |
| 80 | +// oLatency before occurence |
| 81 | +// also schedule the actual event to occur |
| 82 | +// and set up a handle for the OSC return handler to |
| 83 | +// fill in any predicted fields |
| 84 | + |
| 85 | +( |
| 86 | +~oLatency = 0.1; // must match value at tidal boot |
| 87 | +~ncLatency = ~oLatency-0.02; // slightly shorter than oLatency |
| 88 | +~last_t = Process.elapsedTime; |
| 89 | + |
| 90 | +~events = Dictionary.new; |
| 91 | +~event_idx = 0; |
| 92 | + |
| 93 | +// handle OSC input from tidal |
| 94 | +OSCdef(\from_tidal, { |
| 95 | + arg msg, time, src; |
| 96 | + var args = Dictionary.newFrom(msg[1..]); |
| 97 | + // `time` is the start time |
| 98 | + // args[\delta] is the duration |
| 99 | + \from_tidal.postln; |
| 100 | + time.postln; |
| 101 | + // [time - Process.elapsedTime].postln; |
| 102 | + args.postln; |
| 103 | + |
| 104 | + // note-on events should arrive from tidal in the order they happen |
| 105 | + // but note-offs do not, since they are implicit in duration of notes |
| 106 | + // the only guarantee is nothing needs to happen sooner than tidal's |
| 107 | + // oLatency parameter |
| 108 | + // so we can schedule all note-on and note-offs as soon as they arrive |
| 109 | + // (without all sub-events filled in, possibly) |
| 110 | + |
| 111 | + // anthing scheduled sooner than ncLatency (e.g. 100ms) |
| 112 | + // is 'frozen' and ready to (query_)feed notochord. |
| 113 | + |
| 114 | + // schedule note-on |
| 115 | + ~schedule_event.( |
| 116 | + time, args[\ncinst], args[\ncpitch], args[\ncvel] |
| 117 | + ); |
| 118 | + // schedule note-off |
| 119 | + ~schedule_event.( |
| 120 | + time+args[\delta], args[\ncinst], args[\ncpitch], 0 |
| 121 | + ); |
| 122 | + |
| 123 | + // pop those off in order of occurence, and call feed or query_feed |
| 124 | + // in the case of query_feed, pass in some handle which tells the |
| 125 | + // return OSCdef where to put the result in the scheduler |
| 126 | + |
| 127 | + // finally pop completed events as they cross the current time, |
| 128 | + |
| 129 | + |
| 130 | + //when instrument, pitch or velocity is negative or nil, |
| 131 | + //query notochord |
| 132 | + |
| 133 | +}, "/notochord/tidal_feed"); |
| 134 | + |
| 135 | +// create event handle, |
| 136 | +// schedule processing by notochord, |
| 137 | +// and schedule triggering the event |
| 138 | +~schedule_event = { |
| 139 | + arg time, inst, pitch, vel; |
| 140 | + |
| 141 | + var handle = ~event_idx.asSymbol; |
| 142 | + var event = (inst:inst?(-1), pitch:pitch?(-1), vel:vel?(-1)); |
| 143 | + ~events[handle] = event; |
| 144 | + ~event_idx = ~event_idx+1; |
| 145 | + ("scheduling event "++~event_idx).postln; |
| 146 | + |
| 147 | + // schedule notochord processing |
| 148 | + SystemClock.schedAbs(time-~ncLatency, { |
| 149 | + // compute dt from previous event |
| 150 | + event[\time] = time - ~last_t; |
| 151 | + \processing.postln; event.postln; |
| 152 | + ~last_t = time; |
| 153 | + (event[\time]<0).if{"warning: dt < 0".postln}; |
| 154 | + |
| 155 | + // if any parts not determined |
| 156 | + (event.collect(_<0).values.reduce('||')).if{ |
| 157 | + // query_feed |
| 158 | + // include handle to event for writing the result |
| 159 | + ~python.sendMsg( |
| 160 | + "/notochord/query_feed", \handle, handle, |
| 161 | + *~event_to_query.(event) |
| 162 | + ) |
| 163 | + }{ |
| 164 | + // else feed |
| 165 | + ~python.sendMsg( |
| 166 | + "/notochord/feed", *event.asPairs |
| 167 | + ) |
| 168 | + } |
| 169 | + }); |
| 170 | + |
| 171 | + // schedule event |
| 172 | + SystemClock.schedAbs(time, { |
| 173 | + // send to the synthesizer (MIDI bus or hacked dirth synth?) |
| 174 | + \playing.postln; event.postln; |
| 175 | + }); |
| 176 | + |
| 177 | +}; |
| 178 | + |
| 179 | +// convert an event (constructed in schedule_event from data received from tidal) |
| 180 | +// to query keyword args for notochord |
| 181 | +~event_to_query = { |
| 182 | + arg event; |
| 183 | + var query = List[]; |
| 184 | + (event[\inst]>=0).if{query.add(\fix_instrument); query.add(event[\inst])}; |
| 185 | + (event[\pitch]>=0).if{query.add(\fix_pitch); query.add(event[\pitch])}; |
| 186 | + (event[\vel]>=0).if{query.add(\fix_vel); query.add(event[\vel])}; |
| 187 | + query.add(\fix_time); query.add(event[\time]); |
| 188 | + query |
| 189 | +}; |
| 190 | + |
| 191 | + |
| 192 | +// handle OSC return from python |
| 193 | +OSCdef(\from_python, { |
| 194 | + arg msg, time, src; |
| 195 | + var args = Dictionary.newFrom(msg[1..]); |
| 196 | + var event = ~events[args[\handle]]; |
| 197 | + |
| 198 | + // update the event |
| 199 | + event[\inst] = args[\instrument]; |
| 200 | + event[\pitch] = args[\pitch]; |
| 201 | + event[\vel] = args[\velocity]; |
| 202 | + |
| 203 | + ~events.removeAt(\handle); |
| 204 | + |
| 205 | + |
| 206 | + // TODO: spit a warning if it trying to update |
| 207 | + // an event which should have happened already) |
| 208 | + // (thisThread.seconds > ...).if{ |
| 209 | + // "warning: updating past event".postln; event.postln}; |
| 210 | + |
| 211 | + |
| 212 | +}, "notochord/query_return"); |
| 213 | + |
| 214 | +) |
0 commit comments