@@ -3,19 +3,137 @@ thisProcess.openUDPPort(6789); // from tidal
33~python = NetAddr .new("127.0.0.1" , 9999 ); // to python
44)
55
6- // (
7- // s.waitForBoot{
8- // ~dirt = SuperDirt.new;
9- // ~dirt.loadSoundFiles("~/Music/sample/dirt/*");
10- // ~dirt.start(57120);
11- // };
12- // )
13- //
14- // ~dirt.soundLibrary.getEvent('matrix_bass', 1)
15-
16-
17- // for sending MIDI to ableton
6+ // notochord uses General MIDI (https://en.wikipedia.org/wiki/General_MIDI)
7+ // instrument and drum mappings.
8+ // instrument 1-128 are the GM melodic instruments, 129-256 are drumkits
9+
10+ // Option 1: map up to 16 notochord instruments to specific MIDI channels:
11+ (
12+ ~m1 = MIDIOut .newByName("IAC Driver" , "Bus 1" ).latency_(0 );
13+ // this will automatically restrict the instruments notochord can sample
14+ // to the keys of ~midi_map
15+ // (but make sure not to use any other instruments in tidal either)
16+ ~midi_map = (
17+ // notochord instrument (1-256): midi channel (1-16)
18+ 1 : 1 , //grand piano -> channel 1
19+ 13 : 2 , //marimba -> channel 2
20+ // 19: 3, //rock organ -> channel 3
21+ // 26: 4, //acoustic guitar (steel) -> channel 4
22+ // 39: 5, //synth bass 1 -> channel 5
23+ // 43: 6, //cello -> channel 6
24+ // 47: 7, //harp -> channel 7
25+ // 54: 8, //voice oohs -> channel 8
26+ // 57: 9, //trumpet -> channel 9
27+ 129 :10 , //standard drumkit -> channel 10 (drums)
28+ // 67:11, //tenor sax -> channel 11
29+ // 76:12, //pan flute -> channel 12
30+ // 82:13, //saw lead -> channel 13
31+ // 90:14, //warm pad -> channel 14
32+ // 109:15, //kalimba -> channel 15
33+ // 119:16, //synth drum -> channel 16
34+ );
35+ // this will automatically restrict the pitches notochord can sample
36+ // when using a drum instrument to the keys of ~drum_map
37+ // (but make sure not to use any other drums in tidal either)
38+ // this example is based on the default 808 rack in Ableton
39+ ~drum_map = (
40+ //general MIDI number: desired MIDI number
41+ 36 :36 , //electric bass drum -> C1
42+ 37 :37 , //side stick -> C#1
43+ 40 :38 , //electric snare -> D1
44+ 39 :39 , //hand clap -> D#1
45+ 75 :40 , //clave -> E1
46+ 45 :41 , //low tom -> F1
47+ 42 :42 , //closed hat-> F#1
48+ 47 :43 , //mid tom -> G1
49+ 70 :44 , //maracas -> G#1
50+ 50 :45 , //hi tom -> A1
51+ 46 :46 , //open hat -> A#1
52+ 64 :47 , //low conga -> B1
53+ 63 :48 , //mid conga -> C2
54+ 51 :49 , //ride cymbal 1 -> C#2
55+ 62 :50 , //hi conga -> D2
56+ 56 :51 , //cowbell -> D#2
57+ );
58+ ~release_all = {arg vel=0 ;
59+ [~m1 ].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 channel = ~midi_map [inst]-1 ;
64+ ~is_drum .(inst).if{pitch = ~drum_map [pitch]};
65+ channel.isNil.if{"nil channel in mid_send" };
66+ pitch.isNil.if{"nil pitch in mid_send" };
67+ (vel>0 ).if{
68+ ~m1 .noteOn(channel, pitch, vel);
69+ }{
70+ ~m1 .noteOff(channel, pitch);
71+ };
72+ };
73+ )
74+
75+ // Option 2: send MIDI to fluidsynth
76+ // example fluidsynth terminal commands:
77+ // fluidsynth -a coreaudio -m coremidi -v -o midi.autoconnect=1 -o synth.midi-bank-select=mma ~/Downloads/FluidR3_GM_GS.sf2
78+ // fluidsynth -a coreaudio -m coremidi -v -o midi.autoconnect=1 -o synth.midi-bank-select=xg "/Users/victor/Downloads/Timbres of Heaven (XGM) 4.00(G)/Timbres of Heaven (XGM) 4.00(G).sf2"
79+ (
80+ ~drum_map = nil ;
81+ ~midi_map = nil ;
82+ ~bank_xg = true ;
83+ ~channel_lru = LinkedList .fill(16 , {arg i; i});
84+ ~inst_channels = TwoWayIdentityDictionary .new;
85+ ~sema = Semaphore (1 );
86+
87+ MIDIClient .init;
88+ ~m1 = MIDIOut .newByName("IAC Driver" , "Bus 1" ).latency_(0 );
89+
90+ ~release_all = {arg vel=0 ;
91+ [~m1 ].do{arg port; 128 .do{arg note; 16 .do{arg chan;
92+ port.noteOff(chan, note, vel)}}}
93+ };
94+ ~midi_send = {arg inst, pitch, vel;
95+ var channel;
96+ ~sema .wait;
97+ // check if this instrument has a channel
98+ channel = ~inst_channels .at(inst);
99+ channel.isNil.if{
100+ // if not get least recently used channel
101+ channel = ~channel_lru .popFirst;
102+ // and change the bank+program
103+ ~is_drum .(inst).if{
104+ ~m1 .control(channel, 0 , ~bank_xg .if{120 }{1 }); //drum
105+ ~m1 .control(channel, 32 , 0 );
106+ ~m1 .program(channel, inst-129 ); //program
107+ }{
108+ ~m1 .control(channel, 0 , 0 ); //melodic
109+ ~m1 .control(channel, 32 , 0 );
110+ ~m1 .program(channel, inst-1 ); //program
111+ };
112+ // TODO: anonymous instruments?
113+
114+ }{
115+ ~channel_lru .remove(channel)
116+ };
117+ // send the event
118+ (vel>0 ).if{
119+ ~m1 .noteOn(channel, pitch, vel);
120+ }{
121+ ~m1 .noteOff(channel, pitch);
122+ };
123+ // update the inst / channel mappings
124+ ~channel_lru .add(channel);
125+ ~inst_channels .remove(channel);
126+ ~inst_channels [inst] = channel;
127+ ~sema .signal;
128+ };
129+ )
130+
131+ // Option 3: send MIDI to GM.als
132+ // this is an Ableton set which maps general midi onto ~30 synths
133+ // ask victor for the set. requires Live 9 suite or higher
18134(
135+ ~drum_map = nil ;
136+ ~midi_map = nil ;
19137MIDIClient .init;
20138// MIDIClient.destinations
21139~m1 = MIDIOut .newByName("IAC Driver" , "Bus 1" ).latency_(0 );
@@ -86,57 +204,6 @@ MIDIClient.init;
86204};
87205)
88206
89- // for sending MIDI to fluidsynth
90- // fluidsynth -a coreaudio -m coremidi -v -o midi.autoconnect=1 -o synth.midi-bank-select=mma ~/Downloads/FluidR3_GM_GS.sf2
91- (
92- ~channel_lru = LinkedList .fill(16 , {arg i; i});
93- ~inst_channels = TwoWayIdentityDictionary .new;
94- ~sema = Semaphore (1 );
95-
96- MIDIClient .init;
97- ~m1 = MIDIOut .newByName("IAC Driver" , "Bus 1" ).latency_(0 );
98-
99- ~release_all = {arg vel=0 ;
100- [~m1 ].do{arg port; 128 .do{arg note; 16 .do{arg chan;
101- port.noteOff(chan, note, vel)}}}
102- };
103- ~midi_send = {arg inst, pitch, vel;
104- var channel;
105- ~sema .wait;
106- // check if this instrument has a channel
107- channel = ~inst_channels .at(inst);
108- channel.isNil.if{
109- // if not get least recently used channel
110- channel = ~channel_lru .popFirst;
111- // and change the bank+program
112- ((inst>128 )&&(inst<=256 ))||(inst>=265 ).if{
113- ~m1 .control(channel, 0 , 1 ); //drum
114- ~m1 .control(channel, 32 , 0 );
115- ~m1 .program(channel, inst-128 ); //program
116- }{
117- ~m1 .control(channel, 0 , 0 ); //melodic
118- ~m1 .control(channel, 32 , 0 );
119- ~m1 .program(channel, inst-1 ); //program
120- };
121- // TODO: anonymous instruments?
122-
123- }{
124- ~channel_lru .remove(channel)
125- };
126- // send the event
127- (vel>0 ).if{
128- ~m1 .noteOn(channel, pitch, vel);
129- }{
130- ~m1 .noteOff(channel, pitch);
131- };
132- // update the inst / channel mappings
133- ~channel_lru .add(channel);
134- ~inst_channels .remove(channel);
135- ~inst_channels [inst] = channel;
136- ~sema .signal;
137- };
138- )
139-
140207(
141208~oLatency = 0.1 ; // should match value at tidal boot
142209~ncLatency = ~oLatency -0.02 ; // slightly shorter than oLatency
@@ -145,6 +212,11 @@ MIDIClient.init;
145212~events = Dictionary .new;
146213~event_idx = 0 ;
147214
215+ // check if an instrument is a drumkit
216+ ~is_drum = {arg inst;
217+ ((inst>128 )&&(inst<=256 ))||(inst>=265 )
218+ };
219+
148220// handle OSC input from tidal
149221OSCdef (\from_tidal , {
150222 arg msg, time, src;
@@ -271,6 +343,14 @@ OSCdef(\from_tidal, {
271343 query.add(\min_vel ); query.add(1 )
272344 };
273345 query.add(\fix_time ); query.add(event[\time ]);
346+ ~midi_map .notNil.if{
347+ query.add(\include_instrument );
348+ query.add("%JSON:" ++JSON .stringify(~midi_map .keys.asList))
349+ };
350+ ~drum_map .notNil.if{
351+ query.add(\include_drum );
352+ query.add("%JSON:" ++JSON .stringify(~drum_map .keys.asList))
353+ };
274354 query
275355};
276356
0 commit comments