aboutsummaryrefslogtreecommitdiff
#ifndef UDO_MELSEQUENCING
#define UDO_MELSEQUENCING ##

/*
	Melodic pattern sequencer base
	Slim excerpt for Partial Emergence

	This file is part of the SONICS UDO collection by Richard Knight 2021
		License: GPL-2.0-or-later
		http://1bpm.net
*/

#include "sonics/chords.udo"			; chord data
#include "sonics/sequencing.udo"		; sequencer base

; 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
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_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
gkmel_pause init 0										; pause progression changes

; actions: static actions and pattern references filled by _mel_refreshactions
gSmel_actions[] init 1 


; 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"
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



;-----------------------------opcodes-------------------------------------------------------------------------------

/*
	Refresh the actions list: static actions and pattern references
*/
opcode _mel_refreshactions, 0, 0
	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
endop
_mel_refreshactions() ; initialise


/*
	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


/*
	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_length:k() : 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

/*
	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)
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(48, 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
		

		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))]
		tablew iaction1, index, gimel_action1
		tablew iaction2, index, gimel_action2
		index += 1
	od
endop


/*
	Randomise all section parameters
*/
instr mel_randomise
	_mel_randomise()
	gkmel_futures_refresh_trig = 1
	turnoff
endin


/*
	Initialise the sequencer sections; monitor for gkseq_beat triggers and change sections accordingly
*/
instr _mel_manager
#ifndef MEL_HASINIT
	_mel_randomise()
#end
	
	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

	endif

	kstep init 0
	gkmel_section_change = 0

	
	; do something with gkmel_pause == 0
	if (gkseq_beat == 1 && gkmel_pause = 0) then
		if (kstep == 0) then
			tablecopy gimel_current_notes, gimel_next_notes
			kcurrent = _mel_future_pop:k()
			_mel_currentsectionset(kcurrent)
			gkmel_section_change = 1
		endif
		
		ksectionlength = table:k(_mel_currentsectionget:k(), gimel_lengths)
		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



#end