How to create a popup menu whose menu options are dynamic

Hello!

I’m stuck! Again!

The Menu helpfile example is all well and good if you know ahead of time what you want to put in that contextual menu:

(
~menu = Menu(
    MenuAction("A", { "A selected".postln }),
    MenuAction("B", { "B selected".postln }),
    MenuAction("C", { "C selected".postln }),
).front;
)

but let’s imagine I want to make the menu options programatically contextually. I was hoping that this would work

(
~options = ["a", "b", "c"];

~menu = Menu(
	~options.collect{
		arg x;
		MenuAction(x,{x.postln});
	}
).front;
)

but I get a red ERROR: Message 'asMenuAction' not understood.

I reckon I have to parse the array of MenuAction I collect?

I am a bit stuck, and I reckon it is possible but cannot find how to parse it all?

thanks for any pointer

Hope you didn’t spend too long on this, because you will kick yourself…

~f = {|a,b,c|
	postf("a is : %, b is : %, c is : %\n", a, b, c)
}

~f.([1,2,3]) /// a is : [ 1, 2, 3 ], b is : nil, c is : nil

~f.(*[1,2,3]) ///  a is : 1, b is : 2, c is : 3

By placing a * before the array, it will expand to fill all the arguments.

so…

(
~options = ["a", "b", "c"];
~menu_actions = ~options.collect{|x|  MenuAction(x, {x.postln})};
~menu = Menu( *~menu_actions ).front;
)

It is an incredibly useful feature, but the documentation is really hard to find!
It is mentioned in one small line under the method valueArray of Function.

.valueArray(… args)
Evaluates the FunctionDef referred to by the Function. If the last argument is an Array or List, then it is unpacked and appended to the other arguments (if any) to the Function. If the last argument is not an Array or List then this is the same as the ‘value’ method.
{ |a, b, c| ((a * b) + c).postln }.valueArray([3, 10, 7]);
{ |a, b, c, d| [a, b, c, d].postln }.valueArray([1, 2, 3]);
{ |a, b, c, d| [a, b, c, d].postln }.valueArray(9, [1, 2, 3]);
{ |a, b, c, d| [a, b, c, d].postln }.valueArray(9, 10, [1, 2, 3]);
A common syntactic shortcut:
{ |a, b, c| ((a * b) + c).postln }.value(*[3, 10, 7]);

You can also do stuff like,

~f = {| a, b, c|
	postf("a is : %, b is : %, c is : %\n", a, b, c)
}
~f.valueWithEnvir((b: 20, c: -1, a: 1)) ///  a is : 1, b is : 20, c is : -1

Also, just for completeness, the inverse is like this…

~g = {|v ...a| v.postln; a.postln } // 1 then [2,4]
~g.(1, 2, 4)

Here ...a becomes an array of the remaining args.

2 Likes

I did. But hey, you live and learn… and kicking yourself is a way to get things in I reckon :smiley:

Indeed but funnily enough, wasn’t on my radar until now, nor have I read about it. I knew about ... and the # to force literal, but this Array manipulation ninja skill would need an intermediate tutorial somewhere I think. At the moment, one learns about it in this sort of context of head-against-the-wall…

that said, you and a few others are amazing are quickly explaining. my skull appreciates :slight_smile:

p

Yes there ought to be some more advanced tutorials, or techniques really, particularly around Function which is secretly rather complex and nuanced.

Just one of my favourites relating to valueWithEnvir

var voice_processing = { |atk, dec, amp, freq, freqWobble, pan, panRange|
	var sig = SomeUgen.ar(atk...)
	... some audio code that uses the arguments ...
}

// keys match args above.
var default_voice = (\atk: 0.2, \dec: 1.2, \amp: -10.dbamp, \freq: 200, \freqWobble: 2.1, \pan: LFNoise2.kr(0.2), \panRange: [-1, 1]);

// each entry (voice) will become an audio channel (or multiple depending on voice_processing)
var voices_spec = [ 
	(\atk: 0.8, \freq: 143),
	(\atk: 0.04, \dec: 0.2),
	(\pan: 0, \freq: LFNoise2.kr(2).range(220, 440))
];

var arr_of_voices = voices_spec.collect{ |spec|
	voice_processing.valueWithEnvir( default_voice ++ spec )
};
var mixed_voices = Mix.ar(arr_of_voices);

Here each new Event in voices_spec becomes a new voice, replacing the default_voice with any specified arguments. Its a nice alternative to multichannel expansion, where only the differences between the voices are specified - which I think is often more musical.

2 Likes

wow so the concatenation of the 2 arrays (++) will overwrite one value because it is sent to the synth after? i.e. if you did

voice_processing.valueWithEnvir( spec ++ default_voice )

it would not work? I am the eternal newbie - and thanks for sharing this has solved another very long-winded solution I had found to do something similar…

(\a: 1, \b: 2) ++ (\a: 10) // ( 'a': 10, 'b': 2 )
(\a: 10) ++ (\a:1, \b: 2)  // ( 'a': 1, 'b': 2 )

So the ++ on Events work by replacing the values on the left, with the values on the right, but if there are keys on the left that are not in the right, it leaves them.
So in the previous case…

spec ++ default_voice == default_voice

… because spec doesn’t contain unique new keys. Here are some more examples.

(\a: 1, \b: 2) ++ (\a: 10) // ( 'a': 10, 'b': 2 )
(\a: 10, \c: 4) ++ (\a: 1, \b: 2)  // ( 'a': 1, 'b': 2, 'c': 4 )
() ++ (\a: 1) // ('a' : 1)
(\a: 1) ++ (\b: 2) // ('a' : 1, 'b': 2)

What isn’t clear at all from the documentation, is that this works perfectly well with ugens! You can even mix channels this way.

(\freq : SinOsc.kr() ) ++ (\freq: WhiteNoise.kr(1!10)) == (\freq: WhiteNoise.kr(1!10))

To take this to the extreme…

This might be getting off topic now, but it does show how far you can take this concept, and that you don’t have to execute the events in parallel.

Below, I have a default event, then a series of stages, each with a duration.
Each stage lasts for its duration, and will linearly fade into the next stage in \lerpIn time.
If you run this code you should expect amp to move between 10 and 40 for 30 seconds, then interpolate in 3 seconds to moving between 220 and 440.

(
{
	var default = (
		\duration: 20, \lerpIn: 3,
		\freq: LFNoise2.kr(2).range(220, 440), 
		\amp: -15.dbamp
	);
	
	var stage1 = (\duration: 30, \freq: LFNoise2.kr(20).range(10, 40));
	var stage2 = (\duration: 90, \amp: LFNoise2.kr(2).range(0, 1), \lerpIn: 20);
	var stage3 = (\duration: 60, \amp: 0 );
	
	var stages_array_event = [stage1, stage2, stage3].collect{|s| default ++ s};
	var stages_event_array = default.collect{|v, k| stages_array_event.collect{|e| e[k] }  };
	
	var rolling_durations = stages_event_array[\duration].integrate;
	
	var timer = Phasor.kr(0, ControlRate.ir.reciprocal, 0, inf);
	
	var breaks = IEnvGen.kr(Env(rolling_durations.size.collect{|i| i}, rolling_durations), timer);
	var stage_number = breaks.round(1);
	var lerp_time = Select.kr(stage_number, stages_event_array[\lerpIn]);
	var smooth_stage_number = VarLag.kr(stage_number, lerp_time);
	
	var final_event = stages_event_array.collect{ |v, k|
		LinSelectX.kr(smooth_stage_number, v)
	};
	
	var amp = final_event[\freq].poll; 

/// or more helpfully...
// ugenFunction.valueWithEnvir(final_event)

	
}.play
)

I’ve built (and am currently building) whole multimedia works where each event is an osc map, which then fade between each other.
Below is a src (microphone amplitude) used to control the amp of a synth. It will last for 40 seconds before fading into the next osc map (whatever that my be). This is done by having the final function (ugenFunction in the above) take a bunch of inputs, and map them to buses.

(
\duration: 40,
'/flucoma/waterGrain/amp' : src['/micIn/amp'],
...
)

One day, I’ll do a longer post about this and share a piece on this forum… right now, answering questions on here is a great way to procrastinate from the PhD…

5 Likes

Wow this is very generous and nowhere as well explained and demonstrated. Thanks so much! Just understanding that passing valueKeyPairs will cast that array as an Event is already something I don’t know where I’d have found that. Maybe in the classdef of Function ? I’ll dig.

I need to find the care emoji but hey, that is FB corrupting my emoji vocab :slight_smile:

Thanks again for this!