What are some approaches for key quantization (in other words, conforming an input signal to an octave scale)? It seems like something could be done with a non-interpolated buffer reader but it might be weighted oddly.
Unsure what your input signal is, or the desired output. I’ll assume you mean a signal on the server, but… audio or control signal? What are the expected values and how do they behave?
In any case, the keyword in SC for converting between a scale representation of pitch and a chromatic one is degreeToKey
. Searching help turns up this UGen:
http://doc.sccode.org/Classes/DegreeToKey.html
hjh
Good points. Maybe this is an X/Y problem. DegreeToKey looks promising but it’s a bit different from what I had in mind.
What I ultimately want is to be able to plug, say, a saw LFO into a frequency parameter, but instead of having a continuous slide between frequencies, it would be stepped based on the scale, both frequency-wise and timing-wise (due to the potentially irregular quantizing).
I think what I’m ultimately looking for is something like KeyToDegree -> round/truncate -> DegreeToKey.
For example, for quantizing the continuous notes to major pentatonic, this is what I’d expect to see:
// Input Key: 0---1---2---3---4---5---6---7---8---9--10--11--12
// Degree: |---I---|----II-----|--III--|--IV---|-----V-----|
// Quantized Key: |---0---|-----2-----|---5---|---7---|-----9-----|
(This would be a truncate and round would probably make more sense)
EDIT: Somehow I didn’t think KeyToDegree was a thing, but apparently it is. It sounds like it may have issues but I’ll give it a shot.
Ah, I see.
You could load one octave’s worth of frequency ratios into a buffer (1 = root, 2 = octave up). Then maybe along these lines (not at the computer so this is untested):
// you'll need to set root to a frequency first
var freqOverRoot = freq / root,
numOctaves = freqOverRoot.log2.floor,
octaveMul = 2 ** numOctaves,
freqOneOctave = freqOverRoot / octaveMul,
index = IndexInBetween.kr(scaleBuf, freqOneOctave).round(1);
freq = Index.kr(scaleBuf, index) * root * octaveMul;
Or load the buffer with multiple octaves and drop the octave math.
hjh
Tested now. This does seem to do it:
s.boot;
b = Buffer.alloc(s, 13, 1);
// load with a just intonation chromatic scale, you can use any
// or diatonic, just make it 8 frames instead of 13
// note that you should include the wraparound "2" value
// so it's 12 chromatic divisions + 1 = 13
b.setn(0, [1, 25/24, 9/8, 6/5, 5/4, 4/3, 45/32, 3/2, 8/5, 5/3, 9/5, 15/8, 2]);
a = {
var freq = MouseX.kr(220, 880, 2, 0.1),
root = 220,
freqOverRoot = freq / root,
numOctaves = freqOverRoot.log2.floor,
octaveMul = 2 ** numOctaves,
freqOneOctave = freqOverRoot / octaveMul,
index = IndexInBetween.kr(b, freqOneOctave).round(1);
freq = Index.kr(b, index) * root * octaveMul;
SinOsc.ar(freq, 0, 0.1).dup
}.play;
a.trace;
a.free;
hjh
This seems to be a very common use case, wouldn’t it be nice to have a UGen or a Pseudo-UGen covering it?
I made this, it seems to work fine ( for an octave repeating scale).
-
It takes a frequency, a buffer with scale degrees (such as Scale.major.degrees), and a root pitch class from 0 to 12.
-
It outputs both the tuned frequency and the tuning ratio ( it would probably be better to have two different ugens for these two )
FreqToScale : MultiOutUGen {
*kr { arg freq=440, scaleBuf=0, rootPitchClass=0;
^this.multiNew('control', freq,scaleBuf,rootPitchClass);
}
*new1 { arg ugen_rate, freq=440, scaleBuf=0, rootPitchClass=0;
var degree = freq.cpsmidi - rootPitchClass % 12;
var tunedDegree = Index.kr(
scaleBuf,
IndexInBetween.kr(scaleBuf,degree).round
);
var tuneInterval = (tunedDegree-degree).midiratio;
var tunedFreq = freq * tuneInterval;
^[tunedFreq,tuneInterval];
}
}
This looks like a good idea, but your implementation seems to assume 12TET, which could be misleading for other temperaments.
True, but i think other temperaments would be fine as long as they can be expressed as midi intervals using floating point numbers, please correct me if I’m wrong.
The main assumption here is that the scale repeats exactly for every octave.
This implementation definitely works. I modified it a bit to get closer to what I’m shooting for:
s.boot;
b = Buffer.alloc(s, 13, 1);
b.setn(
0,
(0..12).collect({|i|
var scale = Scale.majorPentatonic;
var nearestKey = i.keyToDegree(scale, 12).round.degreeToKey(scale, 12);
nearestKey.midicps / 0.midicps
})
)
(
a = {
var freq = SawDPW.kr(
freq: MouseX.kr(1/8, 6, 1, 1),
mul: MouseY.kr(12, 48, 1, 1),
add: 60
).midicps,
root = 220,
freqOverRoot = freq / root,
numOctaves = freqOverRoot.log2.floor,
octaveMul = 2 ** numOctaves,
freqOneOctave = freqOverRoot / octaveMul,
index = IndexInBetween.kr(b, freqOneOctave).round(1);
freq = Index.kr(b, index) * root * octaveMul;
LFTri.ar(freq, 0, 0.1).dup
}.play;
)
a.trace;
a.free;
EDIT: That said, I prefer @elgiano’s approach.
I’m trying to factor out the buffer but it looks like the control signal doesn’t understand keyToDegree
.
(
a = {
var scale = Scale.majorPentatonic;
var inputKey = SawDPW.kr(
freq: MouseX.kr(1/8, 6, 1, 1),
mul: MouseY.kr(12, 48, 1, 1),
add: 60
);
var nearestKey = inputKey.keyToDegree(scale, 12).round.degreeToKey(scale, 12);
LFTri.ar(nearestKey.midicps, 0, 0.1).dup
}.play;
)
Error:
ERROR: Message 'keyToDegree' not understood.
RECEIVER:
Instance of MulAdd { (0x55aafd5944c8, gc=AC, fmt=00, flg=00, set=03)
instance variables [8]
synthDef : instance of SynthDef (0x55aafe459478, size=16, set=4)
inputs : instance of Array (0x55aafe3a7488, size=3, set=2)
rate : Symbol 'control'
synthIndex : Integer 4
specialIndex : Integer 0
antecedents : nil
descendants : nil
widthFirstAntecedents : nil
}
ARGS:
Instance of Scale { (0x55aafe45e7e8, gc=A8, fmt=00, flg=00, set=02)
instance variables [4]
degrees : instance of Array (0x55aafc045300, size=5, set=3)
pitchesPerOctave : Integer 12
tuning : instance of Tuning (0x55aafe45e378, size=3, set=2)
name : "Major Pentatonic"
}
Integer 12
PROTECTED CALL STACK:
Meta_MethodError:new 0x55aafa8f0400
arg this = DoesNotUnderstandError
arg what = nil
arg receiver = a MulAdd
Meta_DoesNotUnderstandError:new 0x55aafa8f23c0
arg this = DoesNotUnderstandError
arg receiver = a MulAdd
arg selector = keyToDegree
arg args = [ Scale([ 0, 2, 4, 7, 9 ], 12, Tuning([ 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0 ], 2.0, "ET12"), "Major Pentatonic"), 12 ]
Object:doesNotUnderstand 0x55aafa179040
arg this = a MulAdd
arg selector = keyToDegree
arg args = nil
a FunctionDef 0x55aafe4a71c8
sourceCode = "{
var scale = Scale.majorPentatonic;
var inputKey = SawDPW.kr(
freq: MouseX.kr(1/8, 6, 1, 1),
mul: MouseY.kr(12, 48, 1, 1),
add: 60
).keyToDegree(scale, 12).round.degreeToKey(scale, 12);
LFTri.ar(inputKey.midicps, 0, 0.1).dup
}"
var scale = Scale([ 0, 2, 4, 7, 9 ], 12, Tuning([ 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0 ], 2.0, "ET12"), "Major Pentatonic")
var inputKey = nil
SynthDef:buildUgenGraph 0x55aafbeda2c0
arg this = SynthDef:temp__28
arg func = a Function
arg rates = nil
arg prependArgs = [ ]
var result = nil
var saveControlNames = [ ControlName P 0 i_out scalar 0 ]
a FunctionDef 0x55aafb6ff680
sourceCode = "<an open Function>"
arg i_out = an OutputProxy
var result = nil
var rate = nil
var env = nil
SynthDef:buildUgenGraph 0x55aafbeda2c0
arg this = SynthDef:temp__28
arg func = a Function
arg rates = nil
arg prependArgs = [ ]
var result = nil
var saveControlNames = nil
a FunctionDef 0x55aafbed8900
sourceCode = "<an open Function>"
Function:prTry 0x55aafabb3900
arg this = a Function
var result = nil
var thread = a Thread
var next = nil
var wasInProtectedFunc = false
CALL STACK:
DoesNotUnderstandError:reportError
arg this = <instance of DoesNotUnderstandError>
Nil:handleError
arg this = nil
arg error = <instance of DoesNotUnderstandError>
Thread:handleError
arg this = <instance of Thread>
arg error = <instance of DoesNotUnderstandError>
Object:throw
arg this = <instance of DoesNotUnderstandError>
Function:protect
arg this = <instance of Function>
arg handler = <instance of Function>
var result = <instance of DoesNotUnderstandError>
SynthDef:build
arg this = <instance of SynthDef>
arg ugenGraphFunc = <instance of Function>
arg rates = nil
arg prependArgs = nil
Function:play
arg this = <instance of Function>
arg target = <instance of Group>
arg outbus = 0
arg fadeTime = 0.02
arg addAction = 'addToHead'
arg args = nil
var def = nil
var synth = nil
var server = <instance of Server>
var bytes = nil
var synthMsg = nil
< closed FunctionDef > (no arguments or variables)
Interpreter:interpretPrintCmdLine
arg this = <instance of Interpreter>
var res = nil
var func = <instance of Function>
var code = "(
a = {
var scale = Scale.m..."
var doc = nil
var ideClass = <instance of Meta_ScIDE>
Process:interpretPrintCmdLine
arg this = <instance of Main>
^^ The preceding error dump is for ERROR: Message 'keyToDegree' not understood.
RECEIVER: a MulAdd
Exactly. Search for degreeToKey, you’ll find it’s implemented by many classes, including UGen, while keyToDegree is not. Looking into the source code for UGen:degreeToKey, you’ll find that it’s using the DegreeToKey UGen… and there isn’t any KeyToDegree UGen.
Long story short: you can’t search an array in a SynthDef without using a buffer. This is because if the search has to be dynamic or controlled by ugens, it has to be performed on the server, and then the server has to get the data in a buffer.
If you want to factor out the buffer from your SynthDef you need to perform your searches on the client, which will probably mean sending back to the language some index (in this case your inputKey) through SendReply/OSCFunc. It gets more complex… so I would prefer using a buffer here
OSCdef('test',{|msg|
var scale = Scale.majorPentatonic;
var key = msg[3].keyToDegree(scale, 12).round.degreeToKey(scale, 12);
a.set(\nearestKey, key.postln);
},"/index");
a = {
var inputKey = SawDPW.kr(
freq: MouseX.kr(1/8, 6, 1, 1),
mul: MouseY.kr(12, 48, 1, 1),
add: 60
);
SendReply.kr(Changed.kr(inputKey,0.1),"/index",inputKey);
LFTri.ar(\nearestKey.kr.midicps, 0, 0.1).dup
}.play
PS:
I’ve tried it with Tuning.partch.semitones and it works fine
I managed to do this without a buffer using IEnvGen. It’s a little bit hacky (12 values and 10 durations) due to my inexperience with arrays (edit: collections) in SC, but it seems to work:
(
{
var scale = Scale.majorPentatonic;
var scaleKeys = scale.as(List);
var envArgs = (scaleKeys ++ 12).inject(nil, {|acc, val|
if(
acc.isNil,
{ [val.dup, []] },
{
var last = acc[0][acc[0].size - 1];
var diff = val - last;
[acc[0] ++ val.dup, acc[1] ++ [diff, 0]]
}
);
});
var env = Env.new(envArgs[0], envArgs[1]);
var indexInput = LFSaw.ar(1/2, 0, 12, 60);
var index = indexInput % 12;
var octave = indexInput - index;
SinOsc.ar((IEnvGen.ar(env, index) + octave).midicps);
}.play;
)
I just put this together and have not tested it with any other scales or temperaments.
Ok! Glad you found something that works for you! It’s actually interesting to define a scale in terms of both notes and durations of each note
A few things about collections and envelopes (which you might already know or not):
Envelopes do .wrapExtend(levels.size-1)
to durations in order to make the durations array the correct size no matter what. It means that you can have an Env([1,2,3,4,5],1),
where all segments will have duration 1; an Env([0,1],[1,2,3,4,5]), where the only segment present will have duration 1 (the durations array gets truncated); or an Env([1,2,3,4,5],[0,1]) where the [0,1] pattern is repeated (in this case it becomes [0,1,0,1,0]).
I see your inject block, and it looks kind of difficult to read. The following are some little things you might not know:
acc[0][acc[0].size - 1]
can be written more simply asacc[0].last
- you can use
differentiate
to get the pairwise differences between every element in a collection (e.g[1,2,3,40].differentiate -> [1,1,1,37]
)
You can use Env.step to simplify your life a lot
(
{
var scale = Scale.majorPentatonic;
var scaleKeys = scale.as(List) ++ 12;
var env = Env.step(scaleKeys,scaleKeys.differentiate);
var indexInput = LFSaw.ar(1/2, 0, 12, 60);
var index = indexInput % 12;
var octave = indexInput - index;
SinOsc.ar((IEnvGen.ar(env, index) + octave).midicps);
}.play;
)
Oh my, excellent tidbits. Thank you!