Turning interval sets into series of adjacent interval pitches

Question 1:
How would I turn a set of intervals into a set of specific pitches, where, starting with 0, the intervals are consecutively summed together as adjacent intervals?

For example:
[ 1, 2, 5, 7, 7 ] → [ 0, 1, 2, 8, 15, 22 ]
[ 7, 7, 5, 2, 1 ] → [ 0, 7, 14, 19, 21, 22 ]

Question 2:
Starting with a set of intervals, how would I create an array of all possible permutations and render all permutations as adjacent interval series?

For example:
Using permute in some way to create all possible permutations rendered as adjacent intervals…

(
x = [ 1, 2,3];
6.do({|i| x.permute(i).postln;});
--> sum intervals starting with 0

So not as:
[ 1, 2, 3 ]
[ 2, 1, 3 ]
[ 3, 2, 1 ]
[ 1, 3, 2 ]
[ 2, 3, 1 ]
[ 3, 1, 2 ]

But rather as:
[ 0, 1, 3, 6 ]
[ 0, 2, 3, 6 ]
[ 0, 3, 5, 6 ]
[ 0, 1, 4, 6 ]
[ 0, 2, 5, 6 ]
[ 0, 3, 4, 6 ]

Question 3:
Are there any good resources online that detail common ways of working with set theory processes in Supercollider?

Any help would be greatly appreciated–thank you!

On questions 1 and 2:

( // Question 2
~i2pPermute = { |anArray| 
	var permute = anArray.size.factorial.collect { |i| 
		anArray.permute(i)
	}; 
	~i2p = { |anArray| ([0] ++ anArray).integrate }; // Question 1
	permute.do { |anArray| ~i2p.(anArray).postln }
}
)

/* Test question 1 */
~i2p.([1, 2, 5, 7, 7])
~i2p.([7, 7, 5, 2, 1])

/* Test question 2 */
~i2pPermute.([1, 2, 3])

For question 3:

1 Like

Thank you so much for you reply!

Currently, as you have it…
/* Test question 2 */
~i2pPermute.([1, 2, 3])

…produces:
[ [ 1, 2, 3 ], [ 2, 1, 3 ], [ 3, 2, 1 ], [ 1, 3, 2 ], [ 2, 3, 1 ], [ 3, 1, 2 ] ]

While plugging in the results of Q2 in Q1 produces:
[ 0, [ 1, 2, 3 ], [ 3, 3, 6 ], [ 6, 5, 7 ], [ 7, 8, 9 ], [ 9, 11, 10 ], [ 12, 12, 12 ] ]

Any chance for it to produce:
[ [ 0, 1, 3, 6 ], [ 0, 2, 3, 6 ], [ 0, 3, 5, 6 ], [ 0, 1, 4, 6 ], [ 0, 2, 5, 6 ], [ 0, 3, 4, 6 ]]

As follows?

(
~i2pPermute = { |anArray| 
	var permute = anArray.size.factorial.collect { |i| 
		anArray.permute(i)
	}; 
	~i2p = { |anArray| ([0] ++ anArray).integrate }; // Question 1
	permute.collect { |anArray| ~i2p.(anArray).postln } // .do -> .collect
}
)

Then

x =  ~i2pPermute.([1, 2, 3])

This returns the following:

→ a Function
[0, 1, 3, 6]
[0, 2, 3, 6]
[0, 3, 5, 6]
[0, 1, 4, 6]
[0, 2, 5, 6]
[0, 3, 4, 6]
→ [[0, 1, 3, 6], [0, 2, 3, 6], [0, 3, 5, 6], [0, 1, 4, 6], [0, 2, 5, 6], [0, 3, 4, 6]]

to get the result again:

x

returns:

→ [[0, 1, 3, 6], [0, 2, 3, 6], [0, 3, 5, 6], [0, 1, 4, 6], [0, 2, 5, 6], [0, 3, 4, 6]]

1 Like

@wearetemporary

In pitch class theory, the range of integer notation is from 0 to 11.
In your code snippets, the range of intervals seems to be from 1 to 12.

Am I right?

Traditionally set theory uses modulo 12 (0-9, t for ten, e for eleven).

The above code also uses 0 as a starting point for actual pitches, but what might be throwing you off is that the initial sets are not sets of notes, but rather sets of intervals. Hence, why all of the conversions are then translated to sets of notes starting with 0 (ie. [1,7,7] being a minor second and two fifths, translating to [0,1,8,15] or C, C#, G#, D#).

Personally, I tend to use concepts like adjacent interval chords, permutations, equality and completeness, much more freely and often prefer not treating octaves as equivalent.

If you want to learn more about these sorts of concepts, I suggest getting ahold of Tom Johnson’s excellent book “Other Harmony”! :slight_smile:

1 Like

Ah, sorry! My question was the result of a momentary misunderstanding. Oh, my mistake!

Thank you for that!
(I have only partially read “Introduction to Post-Tonal Theory” by Joseph Straus and “The Harmony Book” by E. Carter.)

1 Like

It is basically affine space (a kind of simple vector space without origin).It fits pitch representation and the way we talk about intervals.

Unfortunately, sclang never developed a shared/common foundation for music theory.

There were detailed discussions here about pitch, accidental, interval implementations etc

I also wrote a Rational class, since everything in music theory would require a tested/reliable implementation.

I suppose information of that type is rather scattered. You have to take into account that the majority of SC users is probably more active in the field of synthesis and processing than pitch class theory.
I was quite busy with questions of that kind when working in the artistic research project Patterns of Intuition:

https://iem.kug.ac.at/projektseiten/patterns-of-intuition/introduction

Later on, I developed further some ideas which I included in the miSCellaneous_lib quark extension. Check out the enum helpfile. This method can be used for various combinatorial tasks like listing all objects of a certain type which can be applied to chords, melodies, rhythms etc. enum is quite powerful and complicated, it always takes me some time to understand it myself when I look it up again after a while :slight_smile: Personally I like Ex. 2 following an idea of Fabrice Mogini, producing similar melodic shapes. Such enumerations can nicely be used in Pbinds, though I haven’t used it in my own compositions.

For many questions you can probably do most what you want with some basic SC methods and customized helper functions. E.g., there are many unknown Collection methods. These infos are also quite scattered over the help files (Array, ArrayedCollection, SequenceableCollection …). The help files “List Comprehensions” and “J concepts in SC” are also worth studying.

3 Likes

Not sure if anybody posted the simplest way to do this part of it, but:

([0] ++ myIntervalArray).integrate

// or this -- but I bet the ++ way is faster
myIntervalArray.copy.insert(0, 0).integrate

hjh

2 Likes

I think I wrote not one but a few code snippets demonstrating how intervals could work. However, the idea of a music theory framework has not been resumed. (See threads mentioned above)

EDIT
There was a prototype for conversion between (PitchName Accidental) → Interval
It’s very straightforward, with a few hacks here and there to handle the translation.

in my stuff, I have an implementation that pushed Accidental to 72ET (since you’re working with just intonation, rational numbers and 72ET accidentals can make sense)

You’ve been so helpful I feel slightly embarrassed by how generous you’ve been… but will you permit me one more question?

With permute, is there a way to limit the output to only unique permutations?

So for example, with five unique elements the number of permutations are 5! (5x4x3x2x1) = 120, but in the following set [ 7, 7, 5, 2, 1 ] the number of unique permutations are 5!/2!, since one of the two elements occurs twice. This isn’t the end of the world with a set of 5 elements, but once I use your code to run sets with 7+ elements that have duplicate elements, it’ll become very time consuming to manually cull through the results to spot the duplicates.

Any ideas, how best to implement this?

With the help of ChatGPT I was able to come up with this solution:

(
// Function to convert intervals to pitches
~i2p = { |anArray| ([0] ++ anArray).integrate };

// Function to generate unique permutations and convert them to pitches
~i2pPermute = { |anArray|
    var uniquePermutations = Set[];
    var permute;

    // Recursive permutation function
    permute = { |array, n|
        if (n == 0) {
            uniquePermutations.add(array.copy); // Add a copy of the array to avoid reference issues
        } {
            n.do { |i|
                array = array.deepCopy;
                array.swap(i, n-1);
                permute.(array, n-1);
                array.swap(i, n-1);
            }
        }
    };

    permute.(anArray, anArray.size);

    // Convert unique permutations to pitches
    uniquePermutations.collect { |perm| ~i2p.(perm) }
};

// Example usage
~i2pPermute.([1, 2, 2, 5, 7, 7]).do(_.postln);
)

Is this an efficient way of going about it?

like this?

Code:

(
~i2p = { |array| ([0] ++ array).integrate };

~i2pPermute = { |array| 
	var permute = array.size.factorial.collect { |i| 
		array.permute(i)
	};
	permute.collect { |array| ~i2p.(array) }
};

~i2pPermuteUnique = { |array| 
	~i2pPermute.(array).asSet.asArray
}
)

Test:

~permuted = ~i2pPermute.([1, 2, 2]) 
// -> [[0, 1, 3, 5], [0, 2, 3, 5], [0, 2, 4, 5], [0, 1, 3, 5], [0, 2, 4, 5], [0, 2, 3, 5]]
~permuted.size // -> 6

~permutedUnique = ~i2pPermuteUnique.([1, 2, 2]) 
// -> [[0, 2, 3, 5], [0, 2, 4, 5], [0, 1, 3, 5]]
~permutedUnique.size // -> 3


~permuted = ~i2pPermute.([1, 2, 3]) 
// -> [[0, 1, 3, 6], [0, 2, 3, 6], [0, 3, 5, 6], [0, 1, 4, 6], [0, 2, 5, 6], [0, 3, 4, 6]]
~permuted.size // -> 6

~permutedUnique = ~i2pPermuteUnique.([1, 2, 3]) 
// -> [[0, 1, 4, 6], [0, 1, 3, 6], [0, 2, 3, 6], [0, 3, 5, 6], [0, 2, 5, 6], [0, 3, 4, 6]]
~permutedUnique.size // -> 6


~permuted = ~i2pPermute.([1, 2, 2, 5, 7, 7])
~permuted.size // -> 720

~permutedUnique = ~i2pPermuteUnique.([1, 2, 2, 5, 7, 7])
~permutedUnique.size // -> 180

I have not checked which solution is CPU-efficient, but my code snippet is length-efficient.

It can be done with enum like this:

u = [ 7, 7, 5, 2, 1 ];
v = u.asSet.asArray;
y = 5.enum(v, { |x,i,col| col[(0..i-1)].occurrencesOf(x) < u.occurrencesOf(x) });

This might not be super-obvious, no time to go into detail right now, will continue later today. It’s a slight modification of the last example within enum’s helpfile Ex. 1.

This looks a lot like a list comprehension. That way, it would be very flexible to explore other things later.


// a " general"  case
~allCells = { |allowed, n, sum|
    var result;
    if(n.isKindOf(Number)) {n = [n]};
    if(sum.isKindOf(Number)) {sum = [sum]};
    sum.do { |j|
        n.do { |i|
            result = result ++ all {:x,
                x <- (allowed!i).allTuples,
                x.sum == j;
            };
        };
    };
    result
};

// or something else

~cells = all {:[x,y, x], x <- [2,3,4,6,8], y <- (1..x), y != 5 };

I have added sorting the “pool” which result in a lexically ordered output.

u = [ 7, 7, 5, 2, 1 ];
v = u.asSet.asArray.sort;
y = 5.enum(v, { |x,i,col| col[(0..i-1)].occurrencesOf(x) < u.occurrencesOf(x) });

y.do(_.postln)

->

[ 1, 2, 5, 7, 7 ]
[ 1, 2, 7, 5, 7 ]
[ 1, 2, 7, 7, 5 ]
[ 1, 5, 2, 7, 7 ]
[ 1, 5, 7, 2, 7 ]
[ 1, 5, 7, 7, 2 ]
[ 1, 7, 2, 5, 7 ]
[ 1, 7, 2, 7, 5 ]
[ 1, 7, 5, 2, 7 ]
[ 1, 7, 5, 7, 2 ]
[ 1, 7, 7, 2, 5 ]

...

Here is how it works: enum steps through 5 nodes in a zig-zag pattern and performs a check with the given Function at every node. There’s no check at i == 0 as evalAtZero defaults to false.

I think posting the intermediate results clarifies it. I have reduced the size to 4 so that all steps are posted.

(
u = [ 1, 1, 2, 5 ];
v = u.asSet.asArray.sort;
y = 4.enum(v, { |x,i,col|
	var resultOfCheck, colIfCheckIsTrue;
	"i: ".post; i.postln;
	"x: ".post; x.postln;
	"col[(0..i-1)]: ".post; col[(0..i-1)].postln;
	resultOfCheck = col[(0..i-1)].occurrencesOf(x) < u.occurrencesOf(x);
	"resultOfCheck: ".post; resultOfCheck.postln;
	colIfCheckIsTrue = col[(0..i-1)] ++ x;

	resultOfCheck.if {
		(i < 3).if {
			"indermediate result OK, increase i: ".post;
		}{
			"BINGO, solution found: ".post;
		};
		"new col: ".post; colIfCheckIsTrue.postln;
	};
	"-----------".postln;
	resultOfCheck
});
)

...

-> [ [ 1, 1, 2, 5 ], [ 1, 1, 5, 2 ], [ 1, 2, 1, 5 ], [ 1, 2, 5, 1 ], [ 1, 5, 1, 2 ], [ 1, 5, 2, 1 ], [ 2, 1, 1, 5 ], [ 2, 1, 5, 1 ], [ 2, 5, 1, 1 ], [ 5, 1, 1, 2 ], [ 5, 1, 2, 1 ], [ 5, 2, 1, 1 ] ]

BTW: occurrencesOf is one of those very useful but probably not all too commonly used array gems. It simply counts the number of occurrences in an Array based on an equality check. Unfortunately, if you tend to forget these method names – like it happens to me very often – it’s difficult to find them again, especially in this case where there’s an additional possible source of error by spelling :slight_smile:

3 Likes

@wearetemporary

Does my approach solve your problem? Or have I misunderstood your question?