keyToDegree anomaly?

Hi all,

I’m as much fascinated by scales as they confuse me. Now, I have this little piece of code, of which I hoped I would map a range of continuous values to degrees in a scale, i.e. the values passed in mapped and rounded to the next degree, creating a sequence of steps.

(
(0, 0.0001..1).collect { |i|
	var key = i.linexp(0, 1, 20, 20000).cpsmidi.post;
	": ".post;
	key.keyToDegree(Scale.major, 12).postln
}.plot
)

Possibly, I’m really not understanding how keyToDegree's supposed to work but I really can’t make head nor tail of the result: For most input values I get one float as output (no steps, not what I hoped). E.g.:

// input: output
130.90256153388: 75.95128076694
130.91452047502: 75.957260237511
130.92647941616: 75.963239708082
130.93843835731: 75.969219178653
130.95039729845: 75.975198649224
130.96235623959: 75.981178119794
130.97431518073: 75.987157590365
130.98627412187: 75.993137060936
130.99823306301: 75.999116531507

… but then, suddenly, I have range of different input values, that equally return integer 76:

131.01019200416: 76
131.0221509453: 76
131.03410988644: 76
131.04606882758: 76
131.05802776872: 76
131.06998670986: 76
131.081945651: 76
131.09390459215: 76
131.10586353329: 76
131.11782247443: 76
131.12978141557: 76
131.14174035671: 76
131.15369929785: 76
131.165658239: 76

Is that normal? Intended?

Thanks, Stefan

Hi!
Here i try to explain keyToDegree, hope it helps. It looks like there is indeed an anomaly with that 76, which I write about below.

‘key’ and ‘degree’

in keyToDegree:

  • key is a number of semitones from the root of the scale (not a midi note).
  • Degree is the corresponding index in the degrees array (a degree number, like a major third (5 semitones) is the third degree of the major scale, or degree number 2).
Scale.major.degrees
// -> [ 0, 2, 4, 5, 7, 9, 11 ]
0.keyToDegree(Scale.major) // 0.0 
5.keyToDegree(Scale.major) // 3.0 (5 semitones = fourth degree = degree number 3)
-5.keyToDegree(Scale.major)  // -3.0 ?? third-to-last degree in the array
// see Scale.major.degrees.wrapAt(-3) = 7 (7 semitones, the fifth)

So, by looking in the degrees array you get the number of semitones, and that’s what degreeToKey does.

// Scale.major.degrees[degreeNumber] = numberOfSemitones
degreeNumber.degreeToKey(scale) = numberOfSemitones

keyToDegree does the inverse: given how many semitones from the root, it tells you which degree of the scale it is.

// Scale.major.degrees.indexInBetween(numberOfSemitones) = degreeNumber
numberOfSemitones.keyToDegree(scale) = degreeNumber

Now, like .indexInBetween, it also gives you the “fractional” index: that is, if you ask for an interval that is not in the scale, it will tell you how far it is from the closest degree:

// a tritone is halfway between the fourth and the fifth
6.keyToDegree(Scale.major) // -> 3.5

By the way, if you want to round to the closest degree, you can always do that:

6.keyToDegree(Scale.major).round // -> 4: rounds up to the fifth in this case

the anomaly

That fixed 76 looked a bit suspicious… so i looked a bit into it. Long story short:
it’s a problem with the last degree of the scale. Your 131 breaks down to 10 octaves and 11 semitones. Since the degrees array doesn’t contain the octave (in this case degree number 7 = 12 semitones), the approximation doesn’t work for anything between the last degree and the octave.
It’s the same if you try with 11:

11.3.keyToDegree(Scale.major) // 6
11.5.keyToDegree(Scale.major) // 6
11.999.keyToDegree(Scale.major) // 6

So I think this should be fixed in SuperCollider. This works of course:

(Scale.major.degrees ++ 12).indexInBetween(11.9) // 6.9

And since the documentation mentions that the scale is considered octave-repeating, I wouldn’t see any harm in doing that when calculating keyToDegree.

extra: autotune freq

I have a feeling you wanted to, given a frequency, get the midinote of a tuned note in a scale. Here is a way:

var freq = 610;
var rootMidi = 60;
var scale = Scale.major;

var midiInterval = (freq / rootMidi.midicps).ratiomidi;
var closestDegree = midiInterval.keyToDegree(scale).round;
var tunedInterval = closestDegree.degreeToKey(scale);
var autotunedMidi = rootMidi + tunedInterval;
var autotunedFreq = autotunedMidi.midicps;
"% Hz -> tuned to midi % (% Hz)".format(freq, autotunedMidi, autotunedFreq).postln;
2 Likes

Yeah, it’s a bug, from back in the days when “code review” meant “I haven’t tested it thoroughly but it looks OK, so let’s merge” – it looks OK if indexInBetween wraps around at the top, but it doesn’t.

It might be better to add a wrapping index-in-between method, though. The ++ 12 does create some load on the garbage collector.

I’ll file an issue.

hjh

3 Likes

Hi @elgiano , thanks for your reply and sorry for my delayed reaction. Also thanks very much for enlightening on the meaning of keyToDegree resp. degreeToKey - clearing up my confusion on scales quite a bit. As I just had a look again - documentation on degreeToKey (in SimpleNumber) is pretty verbose, yet your explanation makes musical sense to me.
The example you gave is useful. Thanks! Yes, I think it’s pretty much what I’m looking for. Let’s see how far it gets me…

1 Like