Scale documentation incomplete?

I do not know in which categories this post should be, so let’s go for a simple question.
I am trying to add a method to the Scale class, but there are some points that are not very clear, and probably the code and the documentation will need some updating.

  1. Is it possible to define a scale on 2 octaves? It seems not but I would like to be sure. Maybe it exists a hack to do it. If not maybe it would be relevant to add it as an argument of Scale (in that case some support would be welcome).
  2. what about descending scale? It exists a class ScaleAD, but no idea how to use it. No documentation about it. In the help documentation, it is recommended to use Pavaroh class for ascending/descending scales, but this does not fit my need, because I want to work Scale outside Streams. Also, the help documentation talks about descDegrees in Scale.new but this argument does not relate to anything Scale | SuperCollider 3.12.2 Help.

This might be a silly question, but did you check the Scale class file? I found this for ScaleAD:

ScaleAD : Scale {
	var <>descScale;
	*new { | degrees, pitchesPerOctave, descDegrees, tuning, name = "Unknown Scale" |
		^super.new(degrees, pitchesPerOctave, tuning, name)
			.descScale_(Scale(descDegrees, pitchesPerOctave, tuning, name ++ "desc"))
		;
	}
	asStream { ^ScaleStream(this, 0) }
	embedInStream { ScaleStream(this).yield }

}

For the first part of the question, do you mean something like (this isn’t a great example) the harmonic series on C being expressed (approximately) as

1. C G
2. C E G B♭
3. C D E F♯ G A♭ B♭ B♮

and not C D E F♯ G A♭ B♭ B♮ in every octave? I think dividing the octave is integral to the way the scale class works. IMO conditional statements for multiple scales seems more idiomatic than defining a scale across multiple octaves.

The way I’m seeing it is if you have a scale with different pitch class sets across multiple octaves, you would derive the scale from condensing all of them into one octave. The multiple octave part seems more like a voicing consideration than a characteristic of a scale.
Take the harmonic series as an example again. As a scale, it’s C D E F♯ G A♭ B♭ B♮. If a jazz band is playing this as a chord change, the scale is the same for everyone. The reason the bass player plays C at the beginning of the measure and probably C or G, but maybe E or possibly B♭ on the next strong beat is not because the scale is different, but because they’re thinking of their role in establishing the harmony this scale belongs with. If it’s CHarmonic scale, they’re not going to try to play B on the downbeats.

Yes you can make a Scale span more than an octave - to make a scale spanning two octaves for example you have to use Tuning and set the octaveRatio key to 4…

This is not the most awesome interface for this though! Celeste Hutchins has contributed this FR over at github: Scale object could better support more scale types · Issue #5983 · supercollider/supercollider · GitHub

1 Like

I don’t use Scale or Tuning personally. It’s straightforward enough to just roll your own equivalent of degreeToFreq for each piece.

1 Like

yah even when I use the pattern system I use my own degreeToFreq. Can’t deal with zero-indexing for scale degrees for one thing. After a lifetime of saying “two five one”, “one four zero”?.. nah

Yes, I did, but an example of use will be appreciated, at least for me. Note I am not too familiar with Stream and I do not plan to use it in a Stream context.

The idea is if the help documentation were right about descDegrees, I think this would be the argument that would be useful. That is to say from Scale, it should be possible to call the related descendant scale, but obviously, this can be done like this:

~ascScale = Scale.myAscScale;
~descScale = Scale.mydescScale;

But there is still the issue of how to manage it.
Thanks @semiquaver for the link to the open issue in GitHub.
It is a bit dated now, and it seems that this topic might fall into oblivion.
YES, this is a good idea to rethink Scale implementation.
For instance, instead of degrees, we can use intervals, then for positive intervals according to their related degree report an ascending step, while for negative intervals a descending step.
The code of Scale is a bit esoteric for me, so I am going to develop this idea with Array.
If it works as expected, I will try to implement it into Scale if I find a way to do it, and especially if it is relevant to do so.
Anyway, the purpose is to create a method to fill arrays of midi notes, or frequencies, as sequences according to given scales.

Thanks also for the trick with octaveRatio.

2 Likes

The class does seem a bit messy. Sorry for the tangent and meant no disrespect, but was curious as to what the rationale in the class design was also. I’m not a musicologist and I expect other will have different thoughts about this, that’s why I brought it up.

Could you give an example of this using Tuning:*new, I cannot get it to work?


I highly doubt you will get an answer as this class hasn’t been touched for 15 years - https://github.com/supercollider/supercollider/blame/ef627ce2c564fe323125234e4374c9c4b0fc7f1d/SCClassLibrary/Common/Collections/Scale.sc#L382


Could you tell us how you want to work with it, rather than how you don’t?

This would require the object to know what the last pitch was. That is not a scale and shouldn’t be included in the design, which is already convoluted enough. But something could be made…

Here is what that might look like…

~createScaleFactory = {
	|ascScale, descScale, startingStep=0, rootFreq=220, octave=0|
	
	if(ascScale.isKindOf(Scale).not, 
		{format("ascScale argument must be a Scale, got a %", ascScale.class).error});
	
	if(descScale.isKindOf(Scale).not, 
		{format("descScale argument must be a Scale, got a %", descScale.class).error});
	
	if(startingStep.isKindOf(Integer).not, 
		{format("startingStep argument must be a Integer, got a %", startingStep.class).error});
	
	if(rootFreq.isKindOf(Number).not, 
		{format("rootFreq argument must be a Number, got a %", rootFreq.class).error});
	
	if(octave.isKindOf(Integer).not, 
		{format("octave argument must be a Integer, got a %", octave.class).error});
	
	(
		\prev_step: startingStep,
		\current_step: startingStep,
		\direction: 0,
		
		\pr_get_scale: {|self|
			if(self[\direction] >= 0, {ascScale}, {descScale})
		},
		
		\apply_interval: {|self, interval|
			if(interval.isKindOf(Integer).not,
				{format("interval must be an integer, got a %", interval.class).error});
			
			self[\current_step] = self[\current_step] + interval;
			self[\direction] = interval.sign;
			self.get_freq()
		},
		
		\get_freq: {|self|
			self.pr_get_scale().degreeToFreq(self[\current_step], rootFreq, octave)
		}
	)
}

~sc = ~createScaleFactory.(Scale.major, Scale.minor)

~sc.apply_interval(1); // call this repeatedly, try -1 or other interval

As an aside, the documentation for Tuning is pretty poor and seems interlaced with ideas from Scale without really showing how they relate.

Tuning.new(tuning, octaveRatio: 2.0, name: "Unknown Tuning")
Creates a Tuning using some or all of the parameters as follows: 
tuning can be the name of a library tuning 
(in which case that tuning is returned); 
an array of floats representing the semitone values of the tuning 
(in which case pitchesPerOctave will be set to the size of the array regardless of the second parameter); 
or nil 
(in which case the default tuning for pitchesPerOctave will be returned).
octaveRatio defaults to 2.0, but can be set differently for stretched or compressed tunings.

Let’s clarify the purpose of this thread.
First of all, I am working to implement a new method (related to the cycle extension) aiming to build a full scale, that is to say, starting to one point and then back to this point through ascending and descending scales (the latest could be different).
Apriori, there are no major difficulties, but I was thinking to apply this method to the class Scale directly more or less like so:

a = Scale.new(#[ 0, 2, 3, 4, 6, 7, 8, 10, 11 ], name: "Messiaen mode 3 opp 3", descDegrees:#[ 0, 1, 2, 4, 5, 6, 8, 9, 10 ]);
a.cycle(ambitus:[ 36, 52 ], root: 38); // plus some other arguments ...
// in order to get as output something like:
-> [ 36, 37, 38, 40, 41, 42, 44, 45, 46, 48, 49, 50, 52, 51, 50, 48, 47, 46, 44, 43, 42, 40, 39, 38 ]

So, the difficulty with Scale seems to come from the documentation which is wrong or unclear (as I mentioned in my first post).

Also, I am working on scales which intervals that extend on 2 octaves.
Obviously, this argument is missing. Making it with Tuning appears like a possible hack, but still do not know how.

Thanks for your code.
This will bring without doubt water to our mill. :slightly_smiling_face:

This isn’t really something Scale should do as its just a list of number that holds no internal state. It might be better to make a PhraseGenerator class, then call cycle on it. Might look like…

PhraseGenerator(asc: Scale.major, desc: Scale.min, rootMidi: 38)
.cycle(ambitus:[ 36, 52 ], starting: 36)

This way, your aren’t adding to the class library, which should be avoided because things might change or you might get name collisions with the core class library or other quarks, and it allows you to add many other methods to be called on PhraseGenerator, e.g., sequence, rand, etc…

This is one point that confuses me.
Scale should be a quark and not in the core.
But thanks for the advice.

When I want to make scales with equal divisions of the octave, I scribble out something like this:

var root = 60.midicps;
var steps = 12;
var scale = steps.collect({ |i|  2 ** (i/steps) });
scale * root

The 2 **... in line three could be replaced with a 3 for equal division of the tritave (if you’re going down the Bohlen-Pierce rabbit hole, for example), 4 if you want equal divisions over two octaves, etc.

It would be easy enough to create a subset of such an array if you wanted a scale with unequal steps as well; you could also generate an array with equal divisions and then two different sets of indices that provide different ascending and descending scales.

1 Like

here’s a scale over 2 octaves

a = Scale([0,1, 2, 3, 5, 6 ,7, 9],pitchesPerOctave:10,tuning: Tuning((0..9)*24/10,octaveRatio:4));
Pbind(*[scale:a,degree:Pseries(0,1,16),dur:0.3,octave:4,legato:2 ]).trace.play

Tunings seem to be expressed in units of equal tempered semitones only so you have so scale them to match the desired “octave” (very annoying and hard to guess at!)

1 Like

Perhaps Tunings would be better expressed as deviation from equal steps given the octave size and pitches per octave. Or alternatively as ratios from the root (this is probably better)

See Tuning>>initClass, there are lots of examples there.

1 Like

Tuning stores pitches as fractional midi note numbers, but one can shift representations back and forth using ratiomidi and midiratio.

1 Like

Finally, I made some kind of compromise, and I choose to focus on the ambitus as an array to ‘generate’ the scale.
For the record, here is the code to manage that.

+ Array {
	asScale { | int1, int2, root, step=0 |
		// this : is an Ambitus
		// int1 is either an array of intervals or a Scale
		// int2 idem but can be nil if ascScale = descScale
		var range, theRoot, initInt, compInt, ref1, ref2, res;
		initInt = if (int1.isKindOf(Scale))
		{ (int1.degrees++[int1.pitchesPerOctave]).differentiate[1..] }
		{ int1 };
		range = initInt.sum;
		theRoot = if (root.asBoolean) { root.mod(range) } { this[0].mod(range) };
		compInt = if (int2.isKindOf(Scale))
		{ (int2.degrees++[int2.pitchesPerOctave]).differentiate[1..] }
		{ int2 };
		compInt = if (compInt.isKindOf(Array) && (range == compInt.asArray.sum)) { compInt } { initInt };
		ref1 = [ theRoot ];
		ref2 = [ theRoot ];
		initInt.size.do{|i| ref1 = ref1.add((ref1.last + initInt[i]).mod(range))};
		compInt.size.do{|i| ref2 = ref2.add((ref2.last + compInt[i]).mod(range))};
		res = (this[0]..this[1]).select{|i| ref1.asList.includes(i.mod(range))}
		++
		(this[1]..this[0]).select{|i| ref2.asList.includes(i.mod(range))}[1..];
		res = res[0..res.size-2];
		^res[step..]++res[..step-1]
	}

}

[ 60, 72 ].asScale(Scale.melodicMinor, Scale.melodicMinorDesc, root:36);
// or with arrays of intervals:
[ 60, 72 ].asScale([ 2, 1, 2, 2, 2, 2, 1 ], [ 2, 1, 2, 2, 1, 2, 2 ], root:36);

This is not transcendent, but this is what I wanted.
I am thinking to develop this idea, focusing on the intervals as going up for positive ones and going down for negative ones, which can be more interesting.
I noted about the possible collisions raised by @jordan, so consider this method as a draft.

1 Like