Qirky / FoxDot

Python driven environment for Live Coding
http://foxdot.org
Other
1.04k stars 137 forks source link

Non-octave repeating scales #93

Closed ajtribick closed 6 years ago

ajtribick commented 6 years ago

As far as I can tell, there's no way to set up scales that repeat at intervals other than one octave, e.g. Bohlen-Pierce. I see that the midi() function potentially supports this but the stepsPerOctave parameter doesn't seem to be used when this function gets called, so always ends up being defaulted to 12.

Qirky commented 6 years ago

Hi ajtribick, sorry I did read this but forgot to reply! The scales system left over from a very early version of FoxDot and your right - it doesn't offer much flexibility. I have started to move some of the midi functionality to the Scale class itself so that they stepsPerOctave would be based on how the scale is defined. In pseudo-code, how would you want to define a scale such as Bohlen-Pierce?

ajtribick commented 6 years ago

Hi, sorry for the delay in reply. I was originally thinking of an option to say that the last entry in the scale data should be treated as the "octave", which would allow experimenting around with systems like that while still retaining octaves as the normal behaviour for the majority of cases (and backward compatibility). So (assuming no preset defined), something like:

# BP, equal temperament, chromatic
Scale.default.set([i*12/13*math.log2(3) for i in range(14)], nonoctave=1)

I guess that would still leave the question of how to modulate between modes and change the root, where you'd presumably still have to carry around a rather awkward factor containing the base-2 logarithm of 3 around.

Perhaps it would be easier for microtonality to use the Supercollider approach of having a separation between scales and tunings, so you could do something like

# BP, equal temperament
Tuning.default.set([i*12/13*math.log2(3) for i in range(14)])
# Lambda mode
Scale.default.set([0,2,3,4,6,7,9,10,12])
# Later on, switch to Walker A mode
Scale.default.set([0,1,2,4,5,7,8,10,11])

This approach might be useful for non-equal temperaments (just intonation, etc.), but maybe it would be overkill?

Qirky commented 6 years ago

I've been looking into this and have begun to add a "tuning" option to scales such that the major and justMajor scales are defined like so:

major = ScalePattern([0,2,4,5,7,9,11], name="major", tuning=Tuning.ET12)
justMajor = ScalePattern([0,2,4,5,7,9,11], name="justMajor", tuning=Tuning.just)

FoxDot generally deals with midi note values and converts them to frequency but I don't know enough about BP scale to use the tuning etc in this way. I'm also a bit confused by [i*12/13*math.log2(3) for i in range(14)] as there are 14 values(as opposed to 13) and the values go up to 17.55, which is above 13.

If you have some Python or pseduo-code for calculating note values using the BP scale then I'm all ears

ajtribick commented 6 years ago

That looks like a good approach to doing the tunings, thanks!

The main part of that list comprehension uses the conversion from frequency ratio to ET12 semitones, which is et12_semitones=12*math.log2(freq_ratio). The octave is a 2:1 frequency ratio, or 12 semitones.

So the ET12 tuning in terms of the number of ET12 semitones could be given as [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] (which has 13 elements), the last element of the list being the interval that gets added/subtracted when you go up or down an octave.

The Bohlen-Pierce scale repeats at a frequency ratio of 3:1, which becomes the new "octave". This is 12*math.log2(3) or 19.01955ish ET12 semitones. Then divide that by 13 to get the interval between notes in the equal temperament BP scale (1.463ish ET12 semitones).

So in the same format that gives ET12 as [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], the equal tempered Bohlen-Pierce is [i*12/13*math.log2(3) for i in range(14)] or approximately [0, 1.463, 2.926, 4.389, 5.852, 7.315, 8.778, 10.241, 11.704, 13.167, 14.630, 16.093, 17.557, 19.020], with the last value in the list being the number of ET12 semitones that get added when you go up or down an "octave".

I hope that clears things up a bit, I should have explained it better the first time round!

Qirky commented 6 years ago

Ok so I've had a go at implementing it, but not added default tuning just yet. If you install the latest version on the GitHub and try out the code below, is this what you are expecting?

p1 >> pluck(P[:13,13:0:-1], oct=3, dur=1/2, sus=2)

Scale.default.set([0,2,3,4,6,7,9,10,12], tuning=[i*12/13*math.log2(3) for i in range(14)])

# Added for convenience
Scale.default.set([0,2,3,4,6,7,9,10,12], tuning=Tuning.bohlen_pierce)

print(Scale.default)
print(Scale.default.tuning)
ajtribick commented 6 years ago

I'm trying out the current master branch, I've come across the following:

The default tuning is [0,1,2,3,4,5,6,7,8,9,10,11] which would need to be [0,1,2,3,4,5,6,7,8,9,10,11,12] to support the variable octave repeat.

There seems to be an issue with changing the scale. As a minimal example, the following code causes a "list index out of range" exception:

Scale.default.set("chromatic")
p1 >> pulse(P[:13])

From what I can tell though, this approach does look like it will do the trick!

Qirky commented 6 years ago

Ah, so it's actually caused by the chromatic scale being set to [0,1,2,3,4,5,6,7,8,9,10,11,12] when it should only go up to 11. The reason that I've not set the tuning to 12 is so that, for example using the major scale, when you use a degree of 7 (which is the 12th semitone) it actually increases the octave value by 1 and then treats the degree as 0. If you set the tuning, you do need to include the last number (i.e. 12) in the tuning, which is stored in the tuning's steps attribute. This should work OK for variable octave repeats... I think heh

>>> Scale.default.set([0, 2, 4, 5, 7, 9, 11], tuning=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
>>> print(Scale.default.tuning)
[0, 1, 2, 3, 4, ,5 ,6, 7, 8, 9, 10, 11]
>>> print(Scale.default.tuning.steps)
12
>>> Scale.default.set([0,2,3,4,6,7,9,10,12], tuning=[i*12/13*math.log2(3) for i in range(14)])
>>> print(Scale.default.tuning)
[0.0, 1.4630423083579902, 2.9260846167159804, 4.38912692507397, 5.852169233431961, 7.31521154178995, 8.77825385014794, 10.241296158505932, 11.704338466863922, 13.167380775221913, 14.6304230835799, 16.09346539193789, 17.55650770029588]
>>> print(Scale.default.tuning.steps)
19.019550008653873
ajtribick commented 6 years ago

Thanks for the implementation, looks pretty good, I think this can be closed now.