Monday, May 31, 2010

On the way... matching pitch and duration

I got a working system, where it responds to the input with events that match both duration (<10% difference) and pitch (less than a semitone difference).

Next step: implement larger gesture analysis and turn from "note off" event response to perpetual "accompaniment" - gesture generation.


(
var numinchannels, samplerate, controlrate, masterVol, bufferArray, longbufferLength, fftbuflength, muteInput, in, out, st_out, timeOffset, pathprefix, inputBoost, oldstarttime, minimalEventDuration, currentEventNumber, onsetOffset, soundInNo, inputOffset; //general variables
var recorder, play, straightPlayer, warpPlayer, rec1, time, playCnt, checkPlayers, checkDuration, phrasePlay, clear, getPhrases, phraseAnalyze, phraseRespond; //synthdefs and functions
var startResp, stopResp, contResp; //responders
var longbuf, fftbuf; //buffers
var durationTriggerValue, dursMult, dursMultRand, stimes, stimesRand, timesEnv, timesRand, rates, ratesRand, pitches, pitchesRand, az, azRand, az2diff, el, elRand, el2diff, modAmts, modAmtsRand, modFreqs, modFreqsRand, atts, attsRand, rels, relsRand, amps, ampsRand; //envelopes
var window1, midiNoteKnob, midiAvKnob, hasFreqAvKnob, ampAvKnob, loudnessAvKnob, amplitudeKnob, starttimeNumber, durationNumber, recButton, playButton, onsetNumber, playNumber, clearButton, phraseNumber, ampTrigLoKnob, ampTrigHiKnob, onsetThreshKnob, noteChangeTrigLoKnob, noteChangeTrigHiKnob; //gui
var startTime, stopTime, midiNote, midiAv, hasFreqAv, ampAv, loudnessAv, amplitude, duration; //continous values
var mainList, phraseList, durPhrases, pitchPhrases, durPitchPhrases; //arrays with events and phrases

// ----------- variables to set ------------------------- // 
in = 0; //number of the input channel
out = 42; //number of the first channel of Ambisonics B-format output
st_out = 0; //number of the first channel of stereo out
masterVol = 0; //in dB
inputBoost = 0; //input amplification in dB
minimalEventDuration = 0.2; //in seconds - to avoid unnecessary retriggering //not active currently??
longbufferLength = 90; //length of rec buffers in seconds
onsetOffset = -0.08; //compensate for onset delay; negative values if onsets are too late; -0.08 seems to be fine at the moment

// INIT

numinchannels = 1; //currently only one input channel is supported
//inputBoost = inputBoost.dbamp; //convert to amp
currentEventNumber = 0; //init event numbering
startTime = 0; //init start time
samplerate = Server.default.sampleRate; //get samplerate
controlrate = samplerate / Server.default.options.blockSize; //get control rate
if (samplerate < 50000, {fftbuflength = 1024}, {if (samplerate < 100000, {fftbuflength = 2048},  {fftbuflength = 4096})}); // set the FFT buffer size // fftbuflength.postln;
longbuf = CtkBuffer.new(size: samplerate * longbufferLength).load(sync: true); //main recording buffer
mainList = Array.newClear(7) ! 1000; //set up the global event list for the buffer; values for each array inside: startTime, midiNote, midiAv, hasFreqAv, ampAv, loudnessAv, duration, !!!ADD PAUSEtime---<<--
phraseList = Array.newClear(7) ! 1000; //list of detected phrases
//durPhrases = Array.new; //list of phrases with similar dur
//pitchPhrases = Array.new; //list of phrases with similar pitch
timeOffset = thisThread.seconds; // get runtime time
inputOffset = in + Server.default.options.numOutputBusChannels;
stopTime = 0;
//controlrate.postln;
fftbuf = Buffer.alloc(Server.default, fftbuflength, 1);

// SYNTHDEFS

// TODO - add freq event triggering to the recorder; then add temporal gesture analysis; then create meaningful answer from the system...

recorder = CtkSynthDef(\recorder, {arg mute = 0, onsetTreshold = 0.24, threshHi = -26, threshLo = -39, averageDelay = 0.07, noteChangeTrigLo = 1.1, noteChangeTrigHi = 300, relaxtime = 0.1, floor = -30; //tresholds to be adjusted
var recTrigger, stopTrigger, ampTrigger, onsetTrigger, noteChange, noteTrigger, sumTrigger, amplitude, timer, hasFreq, freq, trigger, freqSlope, sig, hasFreqAv, ampAv, freqAv, counter, recTrigger0, inGate, chain, localbuf, onsets, midiNote, midiSlope, midiAv, loudness, loudnessAv, controlReci, noteNoteGate, midiGated, chain2, loud;
inGate = Gate.ar(In.ar(inputOffset) * inputBoost.dbamp, mute.neg + 1) * (mute.neg + 1); //incoming sound with mute
amplitude = Lag3UD.ar(RunningSum.rms(inGate), 0, 0.05); //Amplitude.kr(LagUD.ar(inGate, 0.01, 0.4), 0.01, 1.8); //amplitude tracking with "smoothing" TRYYYY!!!!
// amplitude.ampdb.poll(25, \amplitude);
ampTrigger = Schmidt.kr(amplitude, threshLo.dbamp, threshHi.dbamp); //amplitude trigger
// ampTrigger.poll(125, \ampTri);
sig = inGate;//Gate.ar(inGate, trigger); //not gated for now
controlReci = controlrate.reciprocal;
trigger = 1;
localbuf = LocalBuf.new(fftbuflength, 1); //fft buffer
// chain = FFT(localbuf, inGate, 0.5); //start the chain
chain = FFT(fftbuf, inGate, 0.5); //start the chain
// chain2 = FFT.new(LocalBuf.new(1024), inGate, 0.5);
loudness = Clip.kr(Loudness.kr(chain), 0, 64); //loudness analysis
// loudness = 0;
// onsets = Gate.kr(Onsets.kr(chain, onsetTreshold, relaxtime: relaxtime, floor: floor.dbamp), 1); //onset detection, gated and automaticaly "muted"
// onsetTrigger = Lag3UD.kr(Onsets.kr(chain, onsetTreshold, relaxtime: relaxtime, floor: floor.dbamp), 0, 0.1); //onset detection
// onsetTrigger = Onsets.kr(chain, onsetTreshold); //onset detection
onsetTrigger = Lag3UD.kr(Onsets.kr(chain, onsetTreshold), 0, 0.01); //onset detection lagged
// onsetTrigger.poll(25, \onsetT);
// stopTrigger = onsets.neg + 1; 
RecordBuf.ar(sig, longbuf, 0, run: 1, loop: 0, trigger: 1); //recorder
#freq, hasFreq = Tartini.kr(sig); //pitch analysis
// midiNote = Gate.kr(freq.cpsmidi, 1); //convert to midi notes
// midiNote = freq.cpsmidi;
midiNote = Lag3UD.kr(freq.cpsmidi, 0.1, 0.1); //lagged -------- !params
// freqSlope = Slope.kr(freq).abs; //kind of amount of change of freq
noteNoteGate = Schmidt.kr(hasFreq, 0.85, 0.9);
midiGated = Gate.kr(midiNote, noteNoteGate);
midiSlope = (midiGated - DelayL.kr(midiGated, 2, averageDelay)).abs; //amount of change of midi note
// midiSlope = (midiGated - DelayL.kr(midiGated, 2, averageDelay)).abs; //lagged amount of change of midi note
// noteChange = Lag3UD.kr((midiSlope - Delay1.kr(midiSlope)).abs * controlrate, 0, 0); //speed of pitch change
// noteChange = Lag3UD.kr(Clip.kr((midiSlope - DelayL.kr(midiSlope, 2, averageDelay)).abs * controlrate, 0, noteChangeTrigHi * 2), 0.0001, 0.001); //speed of pitch change
noteChange = (midiSlope - DelayL.kr(midiSlope, 2, averageDelay)).abs * controlrate; //speed of pitch change
noteTrigger =  Lag3UD.kr(Schmidt.kr(noteChange, noteChangeTrigLo, noteChangeTrigHi), 0, 0.01); //lagged
// noteTrigger.poll(125, \noteTrig);
sumTrigger = (noteTrigger + onsetTrigger) * ampTrigger;
midiAv = AverageOutput.kr(midiSlope, DelayL.kr(sumTrigger, 2, averageDelay)); //average of midi note change
hasFreqAv = AverageOutput.kr(hasFreq, DelayL.kr(sumTrigger, 2, averageDelay)); //average amount of pitched sounds in the sample
ampAv = AverageOutput.kr(amplitude, DelayL.kr(sumTrigger, 2, averageDelay)); //average amplitude of the sample
loudnessAv = AverageOutput.kr(loudness, DelayL.kr(sumTrigger, 2, averageDelay)); //average lodness
timer = Phasor.kr(1, controlrate.reciprocal, 0, longbufferLength); //timer to report rec time
SendReply.kr(sumTrigger, 'start' , [timer]); //send start times for events
SendReply.kr(Impulse.kr(50), 'cont' , [midiNote, midiAv, hasFreqAv, ampAv, noteChange, amplitude]); //test
SendReply.kr(ampTrigger.neg + 1, 'stop' , [timer]); //send stop times for events
});

straightPlayer = CtkSynthDef(\straightPlayer, {arg dur, buffer = longbuf, startPos, amp = 0, rate = 1, az = 0, el = 0, dir=1, modAmt = 0, modFreq = 0, att = 0.001, rel = 0.001; 
var src, env, enc_w, enc_x, enc_y, enc_z, decoded, mod, secondLeg;
secondLeg = dur - (dur * att) - (dur * rel);
env = EnvGen.kr(Env([0.001, 1, 1, 0.001], [att * dur, secondLeg, rel * dur], \exponential));
az = (az * pi) / 180; //to use az in degrees
el = (el * pi) / 180; //to use el in degrees
mod = SinOsc.ar(modFreq, modAmt);
// src = PlayBuf.ar(numinchannels, buffer, (rate + mod) * BufRateScale.kr(buffer), 1, startPos * numinchannels * samplerate, 0) * env;
src = PlayBuf.ar(numinchannels, buffer, rate * BufRateScale.kr(buffer), 1, startPos * samplerate, 0) * env;
// src = PlayBuf.ar(1, buffer);
# enc_w, enc_x, enc_y, enc_z = BFEncode1.ar(src, az, el, 1, 1, 0);
Out.ar(st_out, B2UHJ.ar(enc_w, enc_x, enc_y, enc_z) * amp.dbamp * masterVol.dbamp); //stereo out
// Out.ar(out, [enc_w, enc_x, enc_y, enc_z] * amp.dbamp * masterVol.dbamp); //ambisonics encoded out
});


// RECORDER CONTROL

rec1 = recorder.new; //initialize recorder
rec1.play; //start recorder


// FUNCTIONS

time = {thisThread.seconds - timeOffset}; //get time relative to start

checkDuration = {arg type; //response player inlined
//type: 0 - stopTime, 1 = startTime
if(type==0, 
{duration = stopTime - startTime; mainList.at(currentEventNumber).putEach(6, duration); phraseRespond.value; if(mainList[currentEventNumber][0] == nil, {mainList.at(currentEventNumber).putEach(0, startTime)}); currentEventNumber = currentEventNumber + 1; {durationNumber.value = duration; recButton.value = 0}.defer},
{if(stopTime < oldstarttime, 
{duration = startTime - oldstarttime; 
// 'oldstarttime: '.post; oldstarttime.postln;
// 'startTime: '.post; startTime.postln;
// 'duration: '.post; duration.postln;
mainList.at(currentEventNumber).putEach(6, duration); phraseRespond.value; if(mainList[currentEventNumber][0] == nil, {mainList.at(currentEventNumber).putEach(0, startTime)}); currentEventNumber = currentEventNumber + 1; {durationNumber.value = duration; recButton.value = 0}.defer})}
);
// getPhrases.value;
// phraseAnalyze;
//phraseRespond.value; //NOT HERE
};
phrasePlay = {arg phraseNo;
var pos, dur;
'phrase no: '.post; phraseNo.postln;
pos = mainList[phraseNo][0];
dur = mainList[phraseNo][6];
'startPos: '.post; pos.post; ', duration: '.post; dur.postln;
straightPlayer.new(0, dur).dur_(dur).startPos_(pos).play;
};

clear = { // free whatever possible = stop execution
rec1.free;
longbuf.free;
fftbuf.free;
startResp.remove;
stopResp.remove;
contResp.remove;
AppClock.sched(0.53, {window1.close});
// Server.default.quit;
// Server.default.boot;
};

phraseRespond = { //find similar phrases and play them... to be rebuild to track directions in current playing and use buffer data to resynthesize (?) meaningful output
durPhrases = Array.new; //list of phrases with similar dur
pitchPhrases = Array.new; //list of phrases with similar pitch
durPitchPhrases = Array.new; //similar dur and pitch combined
(currentEventNumber - 1).do({arg i;
// "dur from phraseRespond: ".post; mainList.at(i).at(6).postln;
// "list at i: ".post; mainList.at(i).postln;
if(mainList.at(currentEventNumber).at(6).notNil, {if(((mainList.at(i).at(6) / mainList.at(currentEventNumber).at(6)).abs > 0.9) && ((mainList.at(i).at(6) / mainList.at(currentEventNumber).at(6)).abs < 1.1), {durPhrases = durPhrases.add(i)})}); //find similar note durations
if(mainList.at(currentEventNumber).at(1).notNil, {if(mainList.at(i).at(1).notNil, {if((mainList.at(i).at(1) - mainList.at(currentEventNumber).at(1)).abs < 1, {pitchPhrases = pitchPhrases.add(i)})})}); //find similar pitches
});
"durPhrases: ".post; durPhrases.postln;
"pitchPhrases: ".post; pitchPhrases.postln;
// if(durPhrases.size>0, {phrasePlay.value(durPhrases.at(durPhrases.size.rand))}); //play for similar durations
// if(pitchPhrases.size>0, {phrasePlay.value(pitchPhrases.at(pitchPhrases.size.rand))}); //play for similar pitches
if((pitchPhrases.size>0) && (durPhrases.size>0), {
durPitchPhrases = pitchPhrases.select({ arg item, i; durPhrases.includes(item) });
"durPitchPhrases: ".post; durPitchPhrases.postln;
if(durPitchPhrases.size>0, {phrasePlay.value(durPitchPhrases.at(durPitchPhrases.size.rand))}); 
});
};


// ------ a simple GUI --------

window1 = Window.new('Improviser parameters').front;
window1.view.decorator=FlowLayout(window1.view.bounds);
midiNoteKnob = EZKnob(window1, 50@90,"midiNote", [0, 127]);
midiAvKnob = EZKnob(window1, 50@90,"midiSlAv", [0, 50]);
hasFreqAvKnob = EZKnob(window1, 50@90,"hasFreq", [0, 1]);
ampAvKnob = EZKnob(window1, 50@90,"ampAv", [0, 1]);
// loudnessAvKnob = EZKnob(window1, 50@90,"loudAv", [0, 80]);
loudnessAvKnob = EZKnob(window1, 50@90,"loudAv", [0, 1100]);
amplitudeKnob = EZKnob(window1, 50@90," amp ", [0, 1]);
recButton = Button(window1, Rect(20,20,50,30)).states_([["waiting", Color.gray, Color.black], ["phrase", Color.black, Color.red]]);
starttimeNumber = EZNumber(window1, 150@20,"s_time (s)", [0, longbufferLength]);
durationNumber = EZNumber(window1, 150@20,"dur (s)", [0, longbufferLength]);
onsetNumber = EZNumber(window1, 150@20,"onset no", [0, 10000]);
// phraseNumber = EZNumber(window1, 150@20,"phrase no", [0, 10000]);
playNumber = EZNumber(window1, 150@20,"play phrase no", [0, 10000], {|ez| phrasePlay.value(ez.value)}, labelWidth: 120);
clearButton = Button(window1, Rect(20,20,70,30)).states_([["kill me", Color.gray, Color.black], ["AAAHHH!!!", Color.red, Color.black]]).action_({ arg butt; clear.value});
window1.view.decorator.nextLine;
ampTrigLoKnob = EZKnob(window1, 50@90,"AmpLo(dB)", [-120, 0], {|ez| rec1.threshLo_(ez.value)});
ampTrigHiKnob = EZKnob(window1, 50@90,"AmpHi(dB)", [-120, 0], {|ez| rec1.threshHi_(ez.value)});
onsetThreshKnob = EZKnob(window1, 50@90,"OnsetThr", [0, 1], {|ez| rec1.onsetTreshold_(ez.value)});
noteChangeTrigLoKnob = EZKnob(window1, 50@90,"NoteTLo", [0, 400], {|ez| rec1.noteChangeTrigLo_(ez.value)});
noteChangeTrigHiKnob = EZKnob(window1, 50@90,"NoteTHi", [0, 900], {|ez| rec1.noteChangeTrigHi_(ez.value)});
//vars ampTrigLoKnob, ampTrigHiKnob, onsetThreshKnob, noteChangeTrigLoKnob, noteChangeTrigHiKnob
//rec1 mute = 0, onsetTreshold = 0.93, threshHi = -46, threshLo = -59, averageDelay = 0.1, noteChangeTrigLo = 20, noteChangeTrigHi = 40, relaxtime = 0.1, floor = -30

// RESPONDERS  
startResp = OSCresponder(nil, 'start', { arg t, r, msg; msg.postln;
oldstarttime = startTime; //remember previous start time
startTime = msg.at(3) + onsetOffset; //get actual start time
checkDuration.value(1); //check if we need to take duration from it
mainList.at(currentEventNumber).putEach(0, startTime); //put start time into the current phrase array
// 'starttrigger starttime: '.post; startTime.postln;
{recButton.value = 1; 
starttimeNumber.value = startTime;
onsetNumber.value = currentEventNumber;
}.defer; //gui control
}).add;

stopResp = OSCresponder(nil, 'stop', { arg t, r, msg;// msg.postln;
// var stoptime;
stopTime = msg.at(3); //get stop time
checkDuration.value(0); //put duration in the array 
// 'stoptime: '.post; stopTime.postln;
// {recButton.value = 0;
// durationNumber.value = duration;
// }.defer; //gui contol
}).add;
contResp = OSCresponder(nil, 'cont', { arg t, r, msg;// msg.postln;
var freqAv, hasFreqAv, ampAv, msgNew;
// [midiNote, midiAv, hasFreqAv, ampAv, loudnessAv, amplitude]
{ 
midiNoteKnob.value = msg.at(3);
midiAvKnob.value = msg.at(4);
hasFreqAvKnob.value = msg.at(5);
ampAvKnob.value = msg.at(6);
loudnessAvKnob.value = msg.at(7);
amplitudeKnob.value = msg.at(8);
}.defer; //gui control
// 'org array: '.post; msg.postln;
msgNew = msg.rotate(-3).clipExtend(5); //extract only useful values from the array
// 'new array: '.post; msgNew.postln;
msgNew.do{arg val, i; mainList.at(currentEventNumber).putEach(i+1, val)};//put parameters into the main array
// 'current event no: '.post; currentEventNumber.postln;
}).add;
)

No comments:

Post a Comment