Parsing array into interpretable string for genetic algorithm

I was wondering whether a solution exists to parse an array of strings and numbers into a string that can be executed by the interpreter. A very simple example like:

[2, 0.1, 440, "SinOscAr", "*", "!"]

would be parsed into:

"(SinOsc.ar(440)*0.1)!2"

The underlying idea is to create a genetic algorithm approach to sound creation, each array being an individual. And if I could avoid having to write a parser for the project, that would save me a lot of time :sweat_smile:

Thanks!

1 Like

Are you receiving the input, in your example [2, 0.1, 440, "SinOscAr", "*", "!"], as a String or a structured collection (for example an Array)? And is there a reason your output must be a string rather than the actual array of SinOsc’s?

Two thoughts on this:

.) Following Brian’s question: if you want to have different data in an Array and then produce a SynthDef therewith, you might want to wrap UGens in Functions, then you can take them over. See these discussions: {}.play on a UGen stored in variable?
https://www.listarc.bham.ac.uk/lists/sc-users-2012/msg22935.html

.) You could also use the preProcessor method for Strings (more hassle)
https://www.listarc.bham.ac.uk/lists/sc-users-2016/msg52531.html

.) You could also poke around with the ‘interpret’ method, but this is also often a complication.

I suppose the first approach could fit your demands. Here an example how to integrate operators like ‘+’ and ‘*’:

// in your application x,y could be items of an array

x = { |a, b| BinaryOpUGen('+', a, b) };

y = { |a, b| BinaryOpUGen('*', a, b) };



u = { x.(LFTri.ar(200), LFTri.ar(163)) * 0.1 }.play  

u.release

v = { y.(LFTri.ar(200), LFTri.ar(163)) * 0.1 }.play  

v.release
1 Like

One of the things that I like to say about programming is that all programming reduces to three questions:

  1. What information do I have? (Arrays that have been manipulated by a genetic algorithm.)
  2. What information do I want? (A SynthDef.)
  3. What operations will transform #1 into #2?

Particularly #3 is not a trivial question (and this is usually an iterative process) – but it all comes down to this.

One catch in this specific case is: If there should be a deterministic relationship between 1 and 2, then 1 needs to be unambiguous.

  • OK: Input A → Output Z, and Input B → Output Z. (In SC, rrand(1, 5) and 1 rrand: 5 are different inputs but compile to the same result.)
  • Not OK: Input A → Output Y and Input A → Output Z.

You might run into problems with the input design that you posted.

It looks like a kind of FORTH style reverse Polish notation – and if they’re already separated into items like this, then the parsing is finished. (Parsing = converting a flat code string into a data structure of tokens – but you already have the tokens.) It would be fairly easy to create a stack and then let the opcodes manipulate the stack:

  • Push 2
  • Push 0.1
  • Push 440
  • SinOsc pulls 440 and pushes the UGen instance.
  • * pulls the SinOsc instance and 0.1 and pushes the * result.
  • etc.

That wouldn’t necessarily give you a code string, but it would give you the UGen graph as a resulting data structure. (Or perhaps the "push"es are all strings.)

BUT: To be unambiguous, you can’t skip arguments.

SinOsc takes two arguments: frequency and phase. In that data structure, how do you know that 0.1 is not the SinOsc phase? Currently, there’s no information to disambiguate. That is: Input A [0.1, 440, "SinOscAr"] may correspond to Output Y SinOsc.ar(440) with a residual of 0.1 to be used in a later operation, or to Output Z SinOsc.ar(440, 0.1). That’s exactly a situation that’s disallowed in compiler design.

So the data structure may need some revision.

hjh

1 Like

Maybe an alternative approach: if you unparse your DNA into valid supercollider syntax, you could write the unparsed code in a file, then load the file with .loadRelative or using the Require quark (https://github.com/scztt/Require.quark).

(
~conv = {
	arg bits=[];
	var str;
	str=format("{(%(%)%%)%%}",bits[3],bits[2],bits[4],bits[1],bits[5],bits[0]);
	str.compile;
};
)

f=~conv.value([2, 0.1, 440, "SinOsc.ar", "*", "!"]);
f.play;

Not super-generic but if your array is well structured, it might work.

A more general solution might follow these lines. It’s extensible by adding to ~opcodes.

(
~stack = (
	stack: Array.new,
	pushItem: { |self, item|
		self[\stack] = self[\stack].add(item);
		self
	},
	popItem: { |self, item|
		var return = self[\stack].last;
		self[\stack] = self[\stack].drop(-1);
		return
	},
	topItem: { |self| self[\stack].last },
	resetToEmpty: { |self|
		self[\stack] = Array.new;
		self
	}
);

~opcodes = Dictionary[
	// entries are "regular expression string" -> { |stack, item| ... }
	// the function should push its result back onto the stack
	// assuming only freq as an argument
	"SinOscAr" -> { |stack, item|
		var freq = stack.popItem;
		stack.pushItem("SinOsc.ar(%)".format(freq));
	},
	// regexp requires + at the end of the []... well, OK
	"^[-*/%&|!<=>?+]+$" -> { |stack, item|
		var a = stack.popItem;
		var b = stack.popItem;
		stack.pushItem("(% % %)".format(a, item, b))
	},
	\default -> { |stack, item|
		stack.pushItem(item)
	}
	// more opcodes as needed
];

~findOpcode = { |item|
	var match = block { |break|
		~opcodes.keysDo { |k, v|
			if(k.respondsTo(\matchRegexp) and: { k.matchRegexp(item) }) {
				break.(k);
			};
		};
		\default
	};
	~opcodes[match]
};

~compile = { |array|
	var stack = ~stack.copy.resetToEmpty;
	array.do { |item|
		item = item.asString;
		~findOpcode.(item).value(stack, item);
	};
	stack.topItem
};
)

~compile.([2, 0.1, 440, "SinOscAr", "*", "!"]);
-> ((SinOsc.ar(440) * 0.1) ! 2)

Note though that [1, 4, "-"] would be 4-1 rather than 1-4 – perhaps surprising to humans but legit if you’re generating the arrays algorithmically.

hjh

2 Likes

First of all, thanks to everyone for their answers, I really appreciate!

The only important part is that I start with a collection of tokens (operators, UGens, numbers), and with it I can create a synth that produces a sound (hopefully since the original arrays will be generated randomly).

You’re totally right. I’ll have a fixed number of arguments for each token (with a default value in case the stack is empty).

Yep! I was inspired by the HP 48 calculator that I used when I was at uni. (HP 48 series - Wikipedia)

This is an excellent starting point, thanks for taking the time!

Together with @jamshark70’s stack, I think we’ve got a good starting point.

I’ll keep you posted as the experiment continues!

1 Like