// title: Sampling Demo with QuNeo - Patch 1 // author: Bruno Ruviaro // description: // Each QuNeo pad triggers a different sound file. Horizontal axis controls pitch bend (off by default). Play/Stop buttons control looping. Allows use of 4 banks (up to 64 samples). // code: // ************************************************** // Sampling Demo (QuNeo) // Patch 2 - Basic Sample Playback with optional pitch bend // Bruno Ruviaro, 2013-07-26 // Revision 2014-05-15 // ************************************************** /* How to run: SELECT ALL (Ctrl + A), EVALUATE (Ctrl + Enter) Each pad triggers a different sound file. Horizontal axis of each pad controls transposition. Play button activates looping. Stop button deactivates looping. Pitch bend optional (off by default). All samples should have the same number of channelss (i.e., all mono, or all stereo). Note: you need to provide a path to a folder with 16 audio files. Write in the path in variable ~folderPath below. Tip: drag the samples folder from a Files window onto a blank part of this SuperCollider file. The path will automatically be copied here. */ // Path to samples folder ~folderPath = "/home/lork/Music/SuperCollider/quneo-sampling/sine-test-samples"; // add pitch bend if desired (up to 0.5) ~pitchBend = 0.0; s.waitForBoot({ var maxRate, minRate, loopSwitch, bufferArray, padArray, rateArray, previousXvalues, quneoChannel, lastTime = 0, count = 0, bank = 36; if(MIDIClient.sources.size==0, { MIDIIn.connectAll }, { "MIDI already connected".postln } ); // Choose here how much transposition is desired for X axis maxRate = 1 + ~pitchBend; minRate = 1 - ~pitchBend; loopSwitch = 0; // 1 is loop ON, 0 is loop OFF quneoChannel = 11; // This next array is needed to smooth out jumpy input from QuNeo's x axis... previousXvalues = Array.newClear(127); // Initialize stuff padArray = Array.newClear(127); bufferArray = Array.newClear(127); rateArray = Array.newClear(127); Buffer.freeAll; Routine.new{ // First read buffers (starting at index 36 of array, to match midi note numbers and make it easy to remember). (~folderPath ++ "/*.wav").pathMatch.do({ arg i, c; bufferArray[c+36] = Buffer.read(s, i) }); // Wait until all buffers are read s.sync; // Now add the SynthDef (note that it will need to know numChannels of buffers) SynthDef("playBuf", {arg bufnum = 0, rate, amp = 0.5, gate = 1, loop = 1; var snd, env; env = EnvGen.ar(Env.asr(0.001, 1, 1), gate: gate, doneAction: 2); snd = PlayBuf.ar( numChannels: bufferArray[36].numChannels, // assuming all samples are same num of channels... bufnum: bufnum, rate: Lag.kr(rate), loop: loop); snd = snd * env * amp; Out.ar(0, snd); }).add; }.play; // MIDI from QuNeo MIDIdef.cc( key: \xpads, func: {arg vel, ccNum; var index, rate, previous, winner; index = (24, 27 .. 69).indexOf(ccNum); index = index + bank; // 0-15 range becomes midi note range previous = previousXvalues[index]; // what was the velocity immediately preceding the current one? // In order to avoid discontinuous leaps, we compare previous x value with current 'candidate': // ... if difference is within acceptable bounds, then candidate "wins"; // ... if difference is too big (too big of a leap), then reject candidate and previous value "wins". case {previous.isNil} {winner = vel; previousXvalues[index] = vel} // first time as the pad is triggered {vel==63} {winner = previous} // this ignores the fall back value of QuNeo pads {(vel - previous).abs <= 5} {winner = vel; previousXvalues[index] = vel} // new value is sensible, OK we take it {(vel - previous).abs > 5} {winner = previous}; // if new value is a big leap, ignore it and use previous // up to the lines above, winner is still a number between 0-127 // below I convert it to appropriate ranges case {winner.isNil} {"rate is now nil".postln} // should never happen, but left here as debugging mechanism {winner<=40} {rate = winner.linexp(0, 40, minRate, 1)} {(winner>40)&&(winner<90)} {rate = 1} {winner>=90} {rate = winner.linexp(90, 127, 1, maxRate,)}; rateArray[index] = rate; // store chosen rate into array to be used once at synth attack time padArray[index].set(\rate, rate); // set new rates as they come in }, ccNum: (24, 27 .. 69), chan: quneoChannel); MIDIdef.noteOn( key: \noteOn, func: {arg vel, note; var index = note; padArray[index] = Synth("playBuf", [ \bufnum, bufferArray[index], \rate, rateArray[index], // attack of note should be at correct rate based on x axis \amp, vel/127, \loop, loopSwitch ]); case { note >= 84 } { bank = 84 } { note >= 68 } { bank = 68 } { note >= 52 } { bank = 52 } { note >= 36 } { bank = 36 }; ["noteON", index, "BANK", bank].postln; }, noteNum: (36..99), chan: quneoChannel); MIDIdef.noteOn(\loop, {arg vel, note; case {note==25} {loopSwitch = 0} {note==26} {loopSwitch = 1}; note.postln; }, [25, 26]); MIDIdef.noteOff( key: \noteOff, func: {arg vel, note; var index = note; padArray[index].release; // release node padArray[index] = nil; previousXvalues[index] = nil; rateArray[index] = nil; // ["noteOFF", index].postln; }, noteNum: (36..99), chan: quneoChannel); "Sampling QuNeo".postln; }); // end of block