# Functions for custom scales

The original reason I tried Sonic Pi was for its xenharmonic capabilities. I’ve written a bunch of variations of functions for building scales, but these are probably more useful for how we usually use Sonic Pi.

``````# Equal Temperament, edx = "equal divisions of x"
define :edx do
| mode = [2,2,1,2,2,2],
tet = 12,
equave = 2,
root = 60 |
notes = [midi_to_hz(root)]
for i in 0..(mode.length-1) do
notes = notes.append(notes[i] * equave ** (1.0*mode[i] / tet))
end
notes.ring.map { |n| hz_to_midi(n) }
end

# Just Intonation
define :ji do
| values = [9/8.0, 5/4.0, 4/3.0, 3/2.0, 5/3.0, 15/8.0],
root = 60 |
notes = [midi_to_hz(root)]
for i in 0..(values.length - 1) do
notes = notes.append(notes[0] * values[i])
end
notes.ring.map { |n| hz_to_midi(n) }
end

# convert midi to sample pitch: values
define :midi_pitch do |notes, center=nil|
if center.nil?
center = notes[0] + (notes[notes.length - 1] - notes[0]) / 2
end

notes.map { |n| n - center }
end

# Usage examples

use_synth :bass_foundation

# Bohlen-Pierce scale, equal tempered (13 equal divisions of tritave)
bp = edx([1]*23, 13, 3, 52)

# Bohlen-Pierce, Moll I (decatonic mode)
moll1 = edx([2,1,2,1,1,2,1,2,1], 13, 3, 60)

# Diatonic 12 TET
play_pattern(edx(), sustain: 0.75)
sleep 0.5

# Diatonic 12 JI
play_pattern(ji(), sustain: 0.75)
sleep 0.5

# `sample` example
10.times do
sample :bass_thick_c, pitch: midi_pitch(moll1).shuffle.tick(:tic1), amp: 1
sleep 1.5
end
sleep 2

# BP Just Intonation
bpj = ji([27/25.0,25/21.0,9/7.0,7/5.0,75/49.0,5/3.0,9/5.0,49/25.0,15/7.0,7/3.0,63/25.0,25/9.0,3/1.0],
48)
play_pattern_timed(bpj.values_at(6, 4, 8, 9) + [:r] + bpj.values_at(3, 2, 4, 6),
[1, 0.5, 0.5, 0.5, 0.5])

sleep 1

# 7 EDO/TET
t7 = edx([1]*10, 7, 2, 54)
play_pattern_timed(t7.values_at(6, 4, 8, 9) + [:r] + t7.values_at(3, 2, 4, 6),
[1, 0.5, 0.5, 0.5, 0.5])
``````

Update: A function for finding rational approximations of values above 1.
`val` is a float, the value to be approximated.
`stop` is the largest allowed denominator.
`match` is the minimum proximity to `val` required for inclusion as an approximation.
Returns an array of size three arrays:
`best_ratios[i][0]` is the numerator of the rational.
`best_ratios[i][1]` is the denominator of the rational.
`best_ratios[i][2]` is a decimal representation of the rational.

``````define :rationalize do |val, stop=17, match=0.1|
n = 2.0
d = 1.0

# Initialize
best = (n/d - val).abs
init = true
n2 = n
while init do
n2 += 1
new = (n2/d - val).abs
if (new > best)
init = false
else
n = n2
end
end
if best <= match
best_ratios = [[n,d,n/d]]
else
best_ratios = []
end

# Main loop
while d < stop do
update = false
n += 1
d += 1
ratio = n/d
if (ratio - val).abs < best
update = true
elsif ratio.abs < val
n += 1
ratio = n/d
if (ratio - val).abs < best
update = true
end
elsif ratio > val
n -= 1
ratio = n/d
if (ratio - val).abs < best
update = true
end
end
if update
best = (ratio - val).abs
if best <= match
best_ratios = best_ratios.append([n,d,ratio])
end
end
end

best_ratios
end
``````

Any feedback for better functions and usage are appreciated.

I can’t offer any help but I do wanna say thanks! This is super cool and I’m messing with it a bit to help with some ear training – would love to get a better sense of different tunings, etc.

Thanks for sharing!

1 Like
``````midinumber=[:r]                 # midinumber[0]=rest
for justnumber in 1..255        # tune to A 440
midinumber[justnumber]=hz_to_midi(11*justnumber)
end

scale=[24,27,30,32,36,40,45,48] # just scale

for i in 0..7
play midinumber[scale[i]]     # test scale
sleep 0.5
end``````
1 Like

@hitsware These are frequencies that scale arithmetically. Could you explain the idea further?

I extended the functions to allow building a complete scale. I didn’t like making an options hash map.
Here “tile” is the size of the equave.

``````define :edx do
| mode = [2,2,1,2,2,2],
tet = 12,
equave = 2,
root = 60,
tile = false |
notes = [midi_to_hz(root)]
for i in 0..(mode.length-1) do
notes = notes.append(notes[i] * equave ** (1.0*mode[i] / tet))
end

if tile != false
initial_notes = notes
current_notes = initial_notes
done = false

while done != true do
current_notes = current_notes.map { |n| n/tile }
notes = current_notes + notes
if notes[0] < midi_to_hz(0)
s = 0
while notes[s] < midi_to_hz(0) do
s += 1
end
notes = notes.drop(s+1)
done = true
end
end

current_notes = initial_notes
done = false

while done != true do
current_notes = current_notes.map { |n| n*tile }
notes = notes + current_notes
if notes[notes.length-1] >= midi_to_hz(128)
s = notes.length - 1
while notes[s] >= midi_to_hz(128) do
s -= 1
end
notes = notes.take(s+1)
done = true
end
end
end

notes.ring.map { |n| hz_to_midi(n) }
end

# Just Intonation
define :ji do
| values = [9/8.0, 5/4.0, 4/3.0, 3/2.0, 5/3.0, 15/8.0],
root = 60,
tile = false |
notes = [midi_to_hz(root)]
for i in 0..(values.length - 1) do
notes = notes.append(notes[0] * values[i])
end

if tile != false
initial_notes = notes
current_notes = initial_notes
done = false

while done != true do
current_notes = current_notes.map { |n| n/tile }
notes = current_notes + notes
if notes[0] < midi_to_hz(0)
s = 0
while notes[s] < midi_to_hz(0) do
s += 1
end
notes = notes.drop(s+1)
done = true
end
end

current_notes = initial_notes
done = false

while done != true do
current_notes = current_notes.map { |n| n*tile }
notes = notes + current_notes
if notes[notes.length-1] >= midi_to_hz(128)
s = notes.length - 1
while notes[s] >= midi_to_hz(128) do
s -= 1
end
notes = notes.take(s+1)
done = true
end
end
end

notes.ring.map { |n| hz_to_midi(n) }
end
``````

We could also make `tile` be its own function:

``````define :tile do |notes, tile = 2|
notes = notes.map { |n| midi_to_hz(n) }

initial_notes = notes
current_notes = initial_notes
done = false

while done != true do
current_notes = current_notes.map { |n| n/tile }
notes = current_notes + notes
if notes[0] < midi_to_hz(0)
s = 0
while notes[s] < midi_to_hz(0) do
s += 1
end
notes = notes.drop(s+1)
done = true
end
end

current_notes = initial_notes
done = false

while done != true do
current_notes = current_notes.map { |n| n*tile }
notes = notes + current_notes
if notes[notes.length-1] >= midi_to_hz(128)
s = notes.length - 1
while notes[s] >= midi_to_hz(128) do
s -= 1
end
notes = notes.take(s+1)
done = true
end
end

notes.ring.map { |n| hz_to_midi(n) }
end
``````
``````midinumber=[:r]#                                  midinumber[0]=rest
for justnumber in 1..255
midinumber[justnumber]=hz_to_midi(11*justnumber)
end#                                              from you ....
values = [1/1, 9/8, 5/4, 4/3, 3/2, 5/3, 5/8, 2/1]
#                                                 to simplify your ' values '
#                                                 find a common denominator
values2=[24/24, 27/24, 30/24, 32/24, 36/24,
40/24, 45/24, 48/24]
#                                                  removing denominator gives
#                                                  frequency ratios
#                                                  since 40 corresponds to A ...
#                                                  multiply everything by 11
#                                                  for A 440 Hz
#                                                  hz_to_midi gives midi note numbers
scale=[24,27,30,32,36,40,45,48]#                   just scale

for i in 0..7
play midinumber[scale[i]]#                      test scale
sleep 0.5
end``````
1 Like

Ok. This creates a lot of unused values, but it looks like it can extend to the entire midi set.

Edit:
Seems I can only extend in one direction. Need to use fractions going backwards where there are then not enough notes.

``````midinumber=[:r]#                                  midinumber[0]=rest
for justnumber in 1..255
midinumber[justnumber]=hz_to_midi(11*justnumber)
end

scale=[24,27,30,32,36,40,45,48,54,60,64,72,80,90,96] #  just scale

for i in 0..14
play midinumber[scale[i]]#                      test scale
sleep 0.5
end
``````

Ok. This creates a lot of unused values,
but it looks like it can extend to the entire midi set.

Yes … And fractional midi numbers

Seems I can only extend in one direction.
Need to use fractions going backwards where
there are then not enough notes.

For lower notes ? … the 11 can become 5.5 or 2.75
Also the 255 can be any number …

1 Like

This is great!

I created a function a while ago to do the same thing, but your’s is better

``````define :micro_tune do |note, is_sample|
# Bohlen–Pierce
cents = [0,
146.30,
292.61,
438.91,
585.22,
731.52,
877.83,
1024.13,
1170.44,
1316.74,
1463.05,
1609.35]

tuned_notes = []
idx = 0
scale_length = 12

if !note.kind_of?(Integer)
note.each do |n|
idx = n - (scale_length * (n / scale_length))
tuned_notes.push(n + (cents[idx] - (idx * 100)) / 100)
end
else
idx = note - (scale_length * (note / scale_length))
tuned_notes.push(note + (cents[idx] - (idx * 100)) / 100)
end

if is_sample
return (cents[idx] - (idx * 100)) / 100
else
return tuned_notes
end
end

live_loop :test_notes do
use_synth :tri
notes = scale(:c4, :chromatic)
play micro_tune(notes.tick, false), amp: 1, sustain: 0.75

sleep 1
end``````
1 Like

In xenharmonics, there’s not a wrong way to do things. I like seeing these different ideas.

I have used Sevish’s Scale Workshop. And we can reformat things as needed with regex or another language. (I like using Sublime Text to just find and replace stuff.)

I’ve had mixed feelings about setting :rest to the 0 index. But I just realized that if I choose a value greater than the max index, I get nil and that gets treated as a rest. So, a similar thing is already kind of built in.

Rest is handy for me since I use numbers for notes ,
so ’ 0 ’ gives no note …

Ah yes, I met Sean on Jamendo in the late 00’s. The scale workshop is very innovative, as is his music.

1 Like

If you like Scale Workshop, you might like this:

``````require "~/ziffers/ziffers.rb"

s = parse_scala '
17/16 + 0.709
9/8 + 1.729
19/16 - 6.365
5/4 + 1.8
21/16 + 2.508
11/8 + 2.704
3/2 - 8.153
13/8 + 5.894
27/16 + 6.167
7/4 + 8.899
15/8 + 8.429
2/1'

# Parsed scale in cents
print s

# Format to semitones
ss = cents_to_semitones(s)
``````

Uses ugly hacks and monkeypatches but should work just fine. Let me know if there is any bugs

Ziffers is numered musical notation, which can also be used for microtonal stuff, it’s not especially made for it so there could be some room for improvement:

``````# Scale can be parsed directly using scale with zplay & loops.
# Remeber to use ' ' to escape \ character in strings
zplay '0 e 2 4 s 5 7', scale: '1\3 2\4 3\5', key: :d3
``````
1 Like

Directly reading scala files is very nice.

It can parse scala but doesnt support scala headers and comments. To parse “old” scala file remove the ! and title & length lines. Other than that these should be supported:

• To specify a ratio, simply write it in the format e.g. `3/2`
• To specify an interval in cents, include a . in the line e.g. `701.9` or `1200.`
• To specify n steps out of m-EDO, write it in the format `n\m`
• To specify arbitrary EDJI values, write it in the format `n\m<p/q>`
• To specify a decimal ratio, include a , in the line e.g. `1,5` or `1,25`
• To specify a monzo enclose the exponents inside a square bracket and a closing angle bracket e.g. `[-1 1 0>`
• You can combine intervals using + e.g. `4/3 + 1.23`
1 Like

i just checked this out & it’s really awesome! thanks for putting it out there

Added `rationalize` function.
Uploaded to GitHub: GitHub - CUBICinfinity/sonic_pi_resources: Scripts, Samples, Synths, etc. for Sonic Pi