From 9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22 Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 13 Apr 2025 18:48:02 +0100 Subject: initial --- site/udo/sequencing_melodic.udo | 807 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 807 insertions(+) create mode 100755 site/udo/sequencing_melodic.udo (limited to 'site/udo/sequencing_melodic.udo') diff --git a/site/udo/sequencing_melodic.udo b/site/udo/sequencing_melodic.udo new file mode 100755 index 0000000..d4a5205 --- /dev/null +++ b/site/udo/sequencing_melodic.udo @@ -0,0 +1,807 @@ +#ifndef UDO_MELSEQUENCING +#define UDO_MELSEQUENCING ## + +/* + Melodic pattern sequencer base + + This file is part of the SONICS UDO collection by Richard Knight 2021, 2022 + License: GPL-2.0-or-later + http://1bpm.net +*/ + + +#include "__config__.udo" ; using fftsize for tuning +#include "chords.udo" ; chord data +#include "sequencing.udo" ; sequencer base +#include "interop.udo" ; for updating host with outvalue +#include "wavetables.udo" ; for tuning + +; if these are set, then don't launch the manager automatically. sequencing_melodic_persistence will load accordingly +#ifdef MEL_INITPATH + #define MEL_HASINIT ## +#end +#ifdef MEL_INITDB + #define MEL_HASINIT ## +#end + +;-------------------------internal-globals-------------------------------------------------------------------------- + +gimel_number init 12 ; number of melodic sections available + +gimel_state ftgen 0, 0, -4, -7, 0 ; state: current section, next section, current_step (gimel_number) +gimel_chords ftgen 0, 0, -gimel_number, -7, 0 ; chord indexes from melodic.udo for each section +gimel_notes ftgen 0, 0, -gimel_number, -7, 0 ; midi note numbers for each section +gimel_lengths ftgen 0, 0, -gimel_number, -7, 0 ; lengths in beats for each section +gimel_action1 ftgen 0, 0, -gimel_number, -7, 0 ; follow action 1 for each section: 0 = same, 1 = next, 2 = previous, 3 = random, 4 = specific section (section index + 4) +gimel_action2 ftgen 0, 0, -gimel_number, -7, 0 ; follow action 2 for each section +gimel_actionthreshold ftgen 0, 0, -gimel_number, -7, 0 ; follow action threshold - below 0.5 is action1, above is action2 +gimel_active ftgen 0, 0, -gimel_number, -7, 0 ; whether each section is active or to be ignored +gimel_importance ftgen 0, 0, -gimel_number, -7, 0 ; arbitrary section importance , 0 to 1 +gimel_mod1 ftgen 0, 0, -gimel_number, -7, 0 ; arbitrary modulation 1, 0 to 1 +gimel_mod2 ftgen 0, 0, -gimel_number, -7, 0 ; arbitrary modulation 2, 0 to 1 +gimel_mod3 ftgen 0, 0, -gimel_number, -7, 0 ; arbitrary modulation 3, 0 to 1 +gimel_mod4 ftgen 0, 0, -gimel_number, -7, 0 ; arbitrary modulation 4, 0 to 1 +gimel_centadd ftgen 0, 0, -gimel_number, -7, 0 ; microtonal midi note additions (0 = no change; 1 = add one semitone; 0.01 = add one cent) + +gimel_future ftgen 0, 0, -8, -7, 0 ; future sections: 8 in the future +gimel_current_notes ftgen 0, 0, -13, -7, 0 ; current notes: index 0 is the length +gimel_next_notes ftgen 0, 0, -13, -7, 0 ; next notes: index 0 is the length +gimel_temp_random ftgen 0, 0, -gimel_number, -7, 0 ; temp storage for pattern randomisation + +gkmel_section_change init 0 ; section change trigger +gkmel_section_change_due init 0 ; how many beats until next section change +gkmel_futures_refresh_trig init 0 ; trigger to set if futures are to be recalculated + +; user modifiable variables +gkmel_pause init 0 ; pause progression changes +gkmel_advance_trig init 0 ; manual progression advance trigger +gSmel_details = "" ; notes associated with progression included in save/load operations + + +; names and references for persistence and introspection: essentially the tables to be saved +gSmel_names[] fillarray "chords", "notes", "lengths", "action1", "action2",\ + "actionthreshold", "active", "importance", "mod1", "mod2", "mod3", "mod4", "centadd" +gimel_fns[] fillarray gimel_chords, gimel_notes, gimel_lengths, gimel_action1, gimel_action2,\ + gimel_actionthreshold, gimel_active, gimel_importance, gimel_mod1, gimel_mod2, gimel_mod3, gimel_mod4, gimel_centadd + + + +;-----------------------------opcodes------------------------------------------------------------------------------- + +/* + Refresh the actions list: static actions and pattern references +*/ +Smel_baseactions[] fillarray "Same", "Next", "Previous", "Random" +gSmel_actions[] init lenarray(Smel_baseactions) + gimel_number +index = 0 +while (index < lenarray(gSmel_actions)) do + if (index < 4) then + gSmel_actions[index] = Smel_baseactions[index] + else + gSmel_actions[index] = sprintf("Section %d", index - 3) + endif + index += 1 +od + + + +/* + Send JSON formatted information on current setup to API host +*/ +/* + legacy version + +instr mel_updatehost ; use p4 for channel? + Sjson = json_init() + Sjson = json_appendvalue(Sjson, "sections", gimel_number) + Sjson = json_appendarray(Sjson, "chordnames", gSchords) + Sjson = json_appendarray(Sjson, "actiontypes", gSmel_actions) + + SjsonFns = json_init() + index = 0 + while (index < lenarray(gimel_fns)) do + SjsonFns = json_appendvalue(SjsonFns, gSmel_names[index], gimel_fns[index]) + index += 1 + od + Sjson = json_appendobject(Sjson, "ftables", SjsonFns) + + io_sendstring("mel_state", Sjson) + turnoff +endin +*/ + + +/* + Send JSON formatted information on current setup to API host +*/ +instr mel_updatehost + icbid = p4 + iJson jsoninit + jsoninsertval iJson, "mel_number", gimel_number + jsoninsertval iJson, "chordnames", gSchords + jsoninsertval iJson, "actiontypes", gSmel_actions + jsoninsertval iJson, "cbid", icbid + + iJsonFns jsoninit + jsoninsertval iJsonFns, gSmel_names, gimel_fns + jsoninsert iJson, "ftables", iJsonFns + jsondestroy(iJsonFns) + io_sendstring("callback", jsondumps(iJson, 1)) + turnoff +endin + + + + + + +/* + Get modulation parameters for current section + + imod1, imod2, imod3, imod4 mel_currentmod + + imod1 modulation parameter 1 + imod2 modulation parameter 2 + imod3 modulation parameter 3 + imod4 modulation parameter 4 +*/ +opcode mel_currentmod, iiii, 0 + icur = table:i(0, gimel_state) + xout table:i(icur, gimel_mod1), table:i(icur, gimel_mod2), table:i(icur, gimel_mod3), table:i(icur, gimel_mod4) +endop + + +/* + Get modulation parameters for current section + + kmod1, kmod2, kmod3, kmod4 mel_currentmod + + kmod1 modulation parameter 1 + kmod2 modulation parameter 2 + kmod3 modulation parameter 3 + kmod4 modulation parameter 4 +*/ +opcode mel_currentmod, kkkk, 0 + kcur = table:k(0, gimel_state) + xout table:k(kcur, gimel_mod1), table:k(kcur, gimel_mod2), table:k(kcur, gimel_mod3), table:k(kcur, gimel_mod4) +endop + + +/* + Get the root midi note from the current section chord + + inote mel_rootnote + + inote root note from current chord +*/ +opcode mel_rootnote, i, 0 + xout table:i(1, gimel_current_notes) +endop + +/* + Get a random midi note from the current section chord + + inote mel_randomnote + + inote random note from current chord +*/ +opcode mel_randomnote, i, 0 + ilen = table:i(0, gimel_current_notes) + index = round(random(1, ilen-1)) + xout table:i(index, gimel_current_notes) +endop + + +/* + Get a random midi note from the current section chord + + knote mel_randomnote + + knote random note from current chord +*/ +opcode mel_randomnote, k, 0 + klen = table:k(0, gimel_current_notes) + kindex = round:k(random:k(1, klen-1)) + xout table:k(kindex, gimel_current_notes) +endop + + +/* + Get the current section at k-rate + + ksection _mel_currentsectionget + + ksection current section +*/ +opcode _mel_currentsectionget, k, 0 + xout table:k(0, gimel_state) +endop + + +/* + Get the next section at k-rate + + ksection _mel_nextsectionget + + ksection next section +*/ +opcode _mel_nextsectionget, k, 0 + xout table:k(0, gimel_future) +endop + + +/* + Set the current section at k-rate + + _mel_currentsectionset ksection + + ksection current section to set +*/ +opcode _mel_currentsectionset, 0, k + ksection xin + tablew ksection, 0, gimel_state +endop + + +/* + Get the current section at init time + + isection _mel_currentsectionget + + usection current section +*/ +opcode _mel_currentsectionget, i, 0 + xout table:i(0, gimel_state) +endop + + +/* + Get the length of the current section in seconds + + iseconds mel_length + + iseconds length in seconds +*/ +opcode mel_length, i, 0 + xout table:i(_mel_currentsectionget:i(), gimel_lengths) * i(gkseq_beattime) +endop + + +/* + Get the length of the current section in seconds + + kseconds mel_length + + kseconds length in seconds +*/ +opcode mel_length, k, 0 + xout table:k(_mel_currentsectionget:k(), gimel_lengths) * gkseq_beattime +endop + + +/* + Get the current MIDI note numbers as an array + inotes[] mel_currentnotes + + inotes[] the note numbers +*/ +opcode mel_currentnotes, i[], 0 + ilen = table:i(0, gimel_current_notes) + iout[] init ilen + index = 0 + while (index < ilen) do + iout[index] = table:i(index+1, gimel_current_notes) + index += 1 + od + xout iout +endop + + + +/* + Get the most important entry from futures table + + kbestindex, kimportance, kbeats mel_future_mostimportant + + kbestindex index in gimel_future + kimportance the importance measure + kbeats number of beats until the event occurs +*/ +opcode mel_future_mostimportant, kkk, 0 + kindex = 0 + kimportance = -9999 + kbestindex = 0 + kbeats = table:k(table:k(0, gimel_state), gimel_lengths) ; current duration base + while (kindex < ftlen(gimel_future)) do + ksection = table:k(kindex, gimel_future) + kimportancetemp = table:k(ksection, gimel_importance) + if (kimportancetemp > kimportance) then + kimportance = kimportancetemp + kbestindex = kindex + endif + kindex += 1 + od + + kindex = 0 + while (kindex < kbestindex) do + kbeats += table:k(table:k(kindex, gimel_future), gimel_lengths) + kindex += 1 + od + + xout kbestindex, kimportance, kbeats ; * gkseq_beattime +endop + + +/* + Get the most important entry from futures table + + ibestindex, iimportance, ibeats mel_future_mostimportant + + ibestindex index in gimel_future + importance the importance measure + ibeats number of beats until the event occurs +*/ +opcode mel_future_mostimportant, iii, 0 + index = 0 + importance = -9999 + ibestindex = 0 + ibeats = table:i(table:i(0, gimel_state), gimel_lengths) ; current duration base + while (index < ftlen(gimel_future)) do + isection = table:i(index, gimel_future) + importancetemp = table:i(isection, gimel_importance) + if (importancetemp > importance) then + importance = importancetemp + ibestindex = index + endif + index += 1 + od + + index = 0 + while (index < ibestindex) do + ibeats += table:i(table:i(index, gimel_future), gimel_lengths) + index += 1 + od + xout ibestindex, importance, ibeats ; * i(gkseq_beattime) +endop + + + +/* + Calculate the next section from a given section + + knext _mel_calculatenext kcurrent + + knext the calculated next section index + kcurrent the section index to base the calculation upon +*/ +opcode _mel_calculatenext, k, k + kthissection xin + knextsection = -1 + + if (random:k(0, 1) <= table:k(kthissection, gimel_actionthreshold)) then + knextaction = table:k(kthissection, gimel_action2) + else + knextaction = table:k(kthissection, gimel_action1) + endif + + + ; if current is not active, go to next ? + kcurrentactive = table:k(kthissection, gimel_active) + if (kcurrentactive == 0 && knextaction == 0) then + knextaction = 1 + endif + + ; same + if (knextaction == 0) then + knextsection = kthissection + + ; next or previous + elseif (knextaction >= 1 && knextaction <= 3) then ; specified action + kcount = 0 + kactive = 0 + knextsection = kthissection + while (kactive == 0 && kcount < gimel_number) do ; loop until active section found or all sections checked + + if (knextaction == 1) then ; next + if (knextsection + 1 > gimel_number - 1) then + knextsection = 0 + else + knextsection += 1 + endif + + elseif (knextaction == 2) then ; previous + if (knextsection -1 < 0) then + knextsection = gimel_number - 1 + else + knextsection -= 1 + endif + endif + + kactive = table:k(knextsection, gimel_active) + kcount += 1 + od + + ; random + elseif (knextaction == 3) then + kindex = 0 + krandmax = 0 + while (kindex < gimel_number) do + if (table:k(kindex, gimel_active) == 1) then + tablew kindex, krandmax, gimel_temp_random + krandmax += 1 + endif + kindex += 1 + od + + knextsection = table:k(round(random(0, krandmax - 1)), gimel_temp_random) + + ; specific section + elseif (knextaction >= 4) then ; specific active pattern + if (table:k(knextaction - 4, gimel_active) == 1) then + knextsection = knextaction - 4 + else + knextsection = kthissection + endif + endif + xout knextsection +endop + + +/* + Set gimel_next_notes from the first entry in the futures table +*/ +opcode _mel_setnextnotes, 0, 0 + knext = table:k(0, gimel_future) + chordmidibyindextof gimel_next_notes, table:k(knext, gimel_chords), table:k(knext, gimel_notes), table:k(knext, gimel_centadd) +endop + + +/* + Pop the next future entry from the futures table, move all future entries down one + and add a new calculated entry accordingly + + kcurrent _mel_future_pop + + kcurrent the current section to be used now +*/ +opcode _mel_future_pop, k, 0 + imax = ftlen(gimel_future) + kcurrent = table:k(0, gimel_future) + + + kindex = 0 + while (kindex < imax - 1) do + tablew table:k(kindex + 1, gimel_future), kindex, gimel_future + kindex += 1 + od + + ; write new last entry + tablew _mel_calculatenext(table:k(kindex, gimel_future)), imax - 1, gimel_future + + _mel_setnextnotes() + + xout kcurrent +endop + + +/* + Recalculate the futures table (in the event of parameters being changed at runtime etc) +*/ +opcode _mel_futures_refresh, 0, O + kindexStart xin ; usually 0, can be a start index (ie 1 leaves the first entry in place) + kindex = kindexStart + imax = ftlen(gimel_future) + ; TODO do first, etc + while (kindex < imax) do + if (kindex == 0) then + kcurrent = table:k(0, gimel_state) ; 0 ; get current, rather than 0... + else + kcurrent = table:k(kindex - 1, gimel_future) + endif + + tablew _mel_calculatenext(kcurrent), kindex, gimel_future + kindex += 1 + od + + _mel_setnextnotes() +endop + + +/* + Set next section, for host control + + p4 section number to set as next +*/ +instr mel_setnextsection + isection = p4 + if (table:i(isection, gimel_active) == 1) then + tablew isection, 0, gimel_future + gkmel_futures_refresh_trig = 2 + endif + turnoff +endin + + +/* + Refresh the futures table, for host control +*/ +instr mel_futures_refresh + gkmel_futures_refresh_trig = 1 + turnoff +endin + + +/* + Randomise all section parameters +*/ +opcode _mel_randomise, 0, 0 + index = 0 + iactives[] init 4 + gimel_lengths + iactivenum = 4 + while (index < gimel_number) do + tablew round(random(0, lenarray(gSchords) - 1)), index, gimel_chords + tablew round(random(4, 8)), index, gimel_lengths + tablew round(random(54, 70)), index, gimel_notes + tablew random(0, 1), index, gimel_actionthreshold + tablew random(0, 1), index, gimel_importance + tablew random(0, 1), index, gimel_mod1 + tablew random(0, 1), index, gimel_mod2 + tablew random(0, 1), index, gimel_mod3 + tablew random(0, 1), index, gimel_mod4 + tablew 0, index, gimel_centadd ; always regular to begin with + + + iactive = round(random(0, 1)) + if (iactive == 1) then + iactives[iactivenum-1] = iactive + iactivenum += 1 + endif + tablew iactive, index, gimel_active + index += 1 + od + + ; set next action to only active sections + index = 0 + while (index < gimel_number) do + iaction1 = iactives[round(random(0, iactivenum))] + iaction2 = iactives[round(random(0, iactivenum))] +;iaction1 = 1 +;iaction2 = 1 + tablew iaction1, index, gimel_action1 + tablew iaction2, index, gimel_action2 + index += 1 + od +endop + + +/* + Randomise all section parameters and update the host +*/ +instr mel_randomise + icbid = p4 + _mel_randomise() + gkmel_futures_refresh_trig = 1 + event_i "i", "mel_updatehost", 0, 1, icbid + turnoff +endin + + +/* + Pause progression, for host control +*/ +instr mel_pause + gkmel_pause = p4 + turnoff +endin + + +/* + Advance progression, for host control +*/ +instr mel_advance + gkmel_advance_trig = 1 + turnoff +endin + + +/* + Advance progression if paused, for host control +*/ +instr mel_advanceifpaused + if (gkmel_pause == 1) then + gkmel_advance_trig = 1 + endif + turnoff +endin + + +/* + Get the length of the current progression, if there are two of the same progression consecutively, sum those + + klength mel_nextchangelength + + klength cumulative length taking into account consecutive same sections +*/ +opcode mel_nextchangelength, k, 0 + kcurrent = _mel_currentsectionget:k() + klength = table:k(kcurrent, gimel_lengths) + + imaxfutures = ftlen(gimel_future) + kindex = 0 + while (kindex < imaxfutures) do + ksection = table:k(kindex, gimel_future) + if (ksection != kcurrent) kgoto complete + klength += table:k(ksection, gimel_lengths) + kindex += 1 + od +complete: + xout klength +endop + + + + +/* + Call Sinstrument when ktrig is fired, for each note (passed as p4) and the current section length accordingly + mel_eachnote Sinstrument, ktrig[, klength = mel_length:k()] + + Sinstrument the instrument name to call + ktrig trigger to active call + klength duration of instrument to call, defaulting to mel_length:k() + +*/ +opcode mel_eachnote, 0, SkJ + Sinstrument, ktrig, klength xin + if (ktrig == 1) then + kdur = (klength == -1 ) ? mel_nextchangelength:k() * gkseq_beattime : klength + kindex = 0 + while (kindex < table:k(0, gimel_current_notes)) do + schedulek Sinstrument, 0, kdur, table:k(kindex + 1, gimel_current_notes) + kindex += 1 + od + endif +endop + +/* + Initialise the sequencer sections; monitor for gkseq_beat triggers and change sections accordingly +*/ +instr _mel_manager +#ifndef MEL_HASINIT + _mel_randomise() +#end + + ksectionlength init 0 + gkmel_futures_refresh_trig init 1 + + if (gkmel_futures_refresh_trig != 0) then + _mel_futures_refresh(gkmel_futures_refresh_trig - 1) ; if gkmel_futures_refresh_trig is 2, then omit first, otherwise recalculate all + gkmel_futures_refresh_trig = 0 + ksectionlength = mel_nextchangelength:k() + endif + + kstep init 0 + gkmel_section_change = 0 + + kmanualadvance = 0 + if (gkmel_advance_trig == 1) then + kmanualadvance = 1 + gkmel_advance_trig = 0 + endif + + if ((gkseq_beat == 1 && gkmel_pause == 0) || kmanualadvance == 1) then + if (kstep == 0 || kmanualadvance == 1) then + kcurrent = _mel_currentsectionget:k() + tablecopy gimel_current_notes, gimel_next_notes + knew = _mel_future_pop:k() + _mel_currentsectionset(knew) + + ; only send if actually changed + if (kcurrent != knew) then + io_send("mel_current", knew) ; send current (from next) + gkmel_section_change = 1 + ksectionlength = mel_nextchangelength:k() + endif + endif + + gkmel_section_change_due = ksectionlength - kstep + + if (kstep < ksectionlength - 1) then ; current step < current length + kstep += 1 + else + kstep = 0 + endif + + endif ; end each beat + + +endin + +#ifndef MEL_HASINIT +alwayson "_mel_manager" +#end + + + +/* + Extend the current notes and convert to frequency, multiplying by powers of two to be used in mel_tune + ifreqs[] _mel_tune_noteprepare inotes[], imult + + ifreqs[] resulting frequencies + inotes[] input midi note numbers + imult number of times to multiply note contents + +*/ +opcode _mel_tune_noteprepare, i[], i[]i + iarr[], imult xin + inew[] init lenarray(iarr) * imult + indexnew = 0 + index = 0 + while (index < lenarray(iarr)) do + ifreq = cpsmidinn(iarr[index]) + index2 = 0 + while (index2 < imult) do + if (index2 > 0) then + inew[indexnew] = ifreq * (2* (index2+1)) + else + inew[indexnew] = ifreq + endif + index2 += 1 + indexnew += 1 + od + + index += 1 + od + xout inew +endop + + +/* + Create a chord with the specified frequencies + aout _mel_tune_chord ifreqs[] [, ifn, index] + + aout resulting chord + ifreqs[] frequencies to play + ifn wavetable to play with, default = gifnSine + index internal index usage for recursion +*/ +opcode _mel_tune_chord, a, i[]oo + ifreqs[], ifn, index xin + ifn = (ifn == 0) ? gifnSine : ifn + aout = oscil(0.1, ifreqs[index], ifn) + if (index < lenarray(ifreqs) - 1) then + aout += _mel_tune_chord(ifreqs, ifn, index + 1) + endif + xout aout +endop + + +/* + Stereo tuning to current melodic sequencer notes + aoutL, aoutR mel_tune ainL, ainR, ifn, imult [, ifftrate, ifftdiv] + + aoutL, aoutR output audio + ainL, ainR input audio + ifn wavetable to use + imult multiples of harmonics to generate in tuning + ifftrate fft size, defaults to config default + ifftdiv fft window division factor (eg 4, 8, 16), defaults to config default +*/ +opcode mel_tune, aa, aaiioo + aL, aR, ifn, imult, ifftrate, ifftdiv xin + ifftrate = (ifftrate == 0) ? giFFTsize : ifftrate + ifftdiv = (ifftdiv == 0) ? giFFTwinFactor : ifftdiv + ifreqs[] _mel_tune_noteprepare mel_currentnotes(), imult + fmods pvsanal _mel_tune_chord(ifreqs, ifn), ifftrate, ifftrate/ifftdiv, ifftrate, 1 + fL1 pvsanal aL, ifftrate, ifftrate/ifftdiv, ifftrate, 1 + fR1 pvsanal aR, ifftrate, ifftrate/ifftdiv, ifftrate, 1 + fL2 pvsmorph fL1, fmods, 0, 1 + fR2 pvsmorph fR1, fmods, 0, 1 + aL1 pvsynth fL2 + aR1 pvsynth fR2 + idel = (ifftrate+2)/sr + aL1 balance aL1, delay(aL, idel) + aR1 balance aR1, delay(aR, idel) + xout aL1, aR1 +endop + +#end -- cgit v1.2.3