Some utilities when working with non tempered scales

Today I stumbled upon the fact that many non western scales, for example :ferahfeza, contain decimal midi values, which means that they are not tempered scales. I explored the topic and wrote some routines. Maybe someone else will find them useful.

The first script gives back all Sonic Pi scales that have decimal midi values:

my_scale_arr = []
scale_names.each do |par_scale|
  scale(:C4, par_scale).each do |par_note|
    if par_note != par_note.to_i
      my_scale_arr.append(par_scale)
      break
    end
  end
end
print my_scale_arr

With the following code you get all midi values, approximated midi names and the frequencies in hertz for a certain scale. I rounded them to 4 decimals, because this is more readable:

print (scale :C4,:ferahfeza).to_a.map{
  |note| [note.round(4),
          note_info(note).midi_string,
          midi_to_hz(note).round(4)]
}

If you want to play chords with such scales, you can either use the exact decimal midi values or you can work with chord_degree. Here is the version with chord_degree:

use_synth :piano
play chord_degree(:iv, :c3, :ferahfeza, 3, invert: 1), decay: 1
sleep 2
1 Like

My latest project is a bass machine where bass riffs or licks can be transposed to other scales or chords. I define the licks as nested arrays. The melody of the lick is defined by positions in a scale instead of notes. This allows to transpose it to another scale. I started with normal midi notes, but then I realized, that I wanted it also to work with non tempered scales. To find the position of any note in any scale, I wrote the following function:

# returns the position of a note in a scale or chord as an array
# first value:  position in scale or chord (0 based), nil when not existing
# second value: relative octave, 0 if the same, positive numbers for octaves above the one of the base note
# function call: f_pos_in_noterange(scale(:A3, :minor), :G3)
def f_pos_in_noterange(par_scale, par_note)
  print "par_scale: #{par_scale}"
  print "#{par_scale.to_a.class}"
  l_result = [nil,0]
  l_base = par_scale[0]
  print "l_base: #{l_base} => #{note_info(l_base).midi_string}"
  print "length: #{par_scale.length}"
  l_pos = 0
  par_scale.each do |par_note_loc|
    print note_info(par_note_loc).pitch_class
    # works also for non tempered scales with decimal midi notes
    if (par_note.to_f % 12.0).round(3) == (par_note_loc.to_f % 12.0).round(3) then
      l_result = [l_pos,
                  (par_note.to_f / 12).round(0) - (par_note_loc.to_f / 12).round(0)]
    end
    l_pos = l_pos + 1
  end
  return l_result
end

This function is handy, because it works with scales, chords and even with self defined rings. Here are some examples how to call it:

print f_pos_in_noterange(scale(:A3, :ferahfeza), scale(:A3, :ferahfeza).to_a[4]) # => [4, 0]
print f_pos_in_noterange(scale(:A3, :minor), :G3) # => [6, -1]
print note_info(scale(:A3, :minor)[6]).midi_string # => G4
print f_pos_in_noterange(chord(:A3, :minor), :G3) # => [nil, 0]
print f_pos_in_noterange(chord(:G3, :minor), :G5) # => [0, 2]
print f_pos_in_noterange((ring 61, 65, 69, 73), 69) # => [2, 0]
print f_pos_in_noterange((ring 61, 65, 69, 71), :A3) # => [2, -1]
print f_pos_in_noterange((ring 61, 65.5, 69, 71), 65.5) # => [1, 0]

Below you find a sound example with the following to parts:
1st) normal bass loop in D and G minor
2nd) bass loop transposed to D and G scale ferahfeza

1 Like

This is how I got started with 31-edo tuning.

Python script to generate array of frequencies, in this case I keep A = 440 Hz

# generate list of frequencies for 31-edo tuning

from decimal import Decimal, getcontext
from collections import OrderedDict
getcontext().prec = 56

freq_a = 440

# devide the range from freq_a to 2 * freq_a in 31 equal devisions according to equal temperament
step_31_edo = Decimal(2) ** (Decimal(1)/Decimal(31))

print(step_31_edo)

frequencies = [Decimal(freq_a)]

for i in range(0,31):
    frequencies.append(frequencies[i] * step_31_edo)

frequencies0 = []
frequencies1 = []
frequencies2 = []
frequencies3 = []
frequencies4 = []
frequencies5 = []
frequencies6 = []
frequencies7 = []
frequencies8 = []

for f in frequencies:
    frequencies0.append(float(str(round(f/16,3))))
    frequencies1.append(float(str(round(f/8,3))))
    frequencies2.append(float(str(round(f/4,3))))
    frequencies3.append(float(str(round(f/2,3))))
    frequencies4.append(float(str(round(f,3))))
    frequencies5.append(float(str(round(f*2,3))))
    frequencies6.append(float(str(round(f*4,3))))
    frequencies7.append(float(str(round(f*8,3))))
    frequencies8.append(float(str(round(f*16,3))))

all_frequencies = frequencies0 + frequencies1 + frequencies2 + frequencies3 + frequencies4 + frequencies5 + frequencies6 + frequencies7 + frequencies8
all_frequencies = list(OrderedDict.fromkeys(all_frequencies)) # remove duplicates, keep ordering

print('f0 = ' +str(frequencies0))
print('f1 = ' +str(frequencies1))
print('f2 = ' +str(frequencies2))
print('f3 = ' +str(frequencies3))
print('f4 = ' +str(frequencies4))
print('f5 = ' +str(frequencies5))
print('f6 = ' +str(frequencies6))
print('f7 = ' +str(frequencies7))
print('f8 = ' +str(frequencies8))

print(' ')

print('freqs = ' + str(all_frequencies))

Use the result in Sonic Pi. I’m setting an offset to 119 where the G3 frequency is in the array and defining a :speel function (dutch word for play) where you can pass an index relative to the offset. so speel [31] will play G4, speel [1] plays G+ (G half sharp). Notes go like this G G+ G# Ab Ad A but it’s easier to think in numbers.

Example melody, first line of Buuvei (lullaby) by Urna Chahar-Tugchi with chords by me:

freqs = [27.5, 28.122, 28.758, 29.408, 30.073, 30.753, 31.448, 32.159, 32.887, 33.63, 34.391, 35.168, 35.963, 36.777, 37.608, 38.459, 39.328, 40.217, 41.127, 42.057, 43.008, 43.98, 44.975, 45.991, 47.031, 48.095, 49.182, 50.294, 51.432, 52.595, 53.784, 55.0, 56.244, 57.515, 58.816, 60.146, 61.506, 62.897, 64.319, 65.773, 67.26, 68.781, 70.336, 71.927, 73.553, 75.216, 76.917, 78.656, 80.435, 82.253, 84.113, 86.015, 87.96, 89.949, 91.983, 94.063, 96.19, 98.365, 100.589, 102.863, 105.189, 107.568, 110.0, 112.487, 115.031, 117.632, 120.292, 123.012, 125.793, 128.637, 131.546, 134.521, 137.562, 140.673, 143.853, 147.106, 150.433, 153.834, 157.312, 160.869, 164.507, 168.227, 172.031, 175.92, 179.898, 183.966, 188.126, 192.38, 196.729, 201.178, 205.727, 210.379, 215.135, 220.0, 224.975, 230.062, 235.264, 240.583, 246.023, 251.586, 257.275, 263.092, 269.041, 275.124, 281.345, 287.707, 294.212, 300.865, 307.668, 314.625, 321.739, 329.014, 336.453, 344.061, 351.841, 359.796, 367.932, 376.251, 384.759, 393.459, 402.356, 411.453, 420.757, 430.271, 440.0, 449.949, 460.123, 470.527, 481.166, 492.046, 503.172, 514.55, 526.184, 538.082, 550.249, 562.691, 575.414, 588.425, 601.73, 615.336, 629.25, 643.478, 658.028, 672.907, 688.122, 703.682, 719.593, 735.864, 752.503, 769.518, 786.918, 804.711, 822.907, 841.514, 860.542, 880.0, 899.898, 920.246, 941.054, 962.333, 984.092, 1006.344, 1029.099, 1052.368, 1076.164, 1100.498, 1125.381, 1150.828, 1176.85, 1203.46, 1230.672, 1258.499, 1286.956, 1316.056, 1345.814, 1376.244, 1407.363, 1439.186, 1471.728, 1505.006, 1539.036, 1573.836, 1609.423, 1645.814, 1683.028, 1721.084, 1760.0, 1799.796, 1840.492, 1882.108, 1924.665, 1968.185, 2012.688, 2058.198, 2104.737, 2152.328, 2200.995, 2250.763, 2301.656, 2353.7, 2406.92, 2461.344, 2516.999, 2573.912, 2632.111, 2691.627, 2752.489, 2814.727, 2878.372, 2943.456, 3010.011, 3078.072, 3147.672, 3218.845, 3291.628, 3366.056, 3442.168, 3520.0, 3599.592, 3680.984, 3764.217, 3849.331, 3936.37, 4025.377, 4116.396, 4209.474, 4304.656, 4401.991, 4501.526, 4603.312, 4707.399, 4813.84, 4922.688, 5033.997, 5147.823, 5264.223, 5383.255, 5504.978, 5629.453, 5756.743, 5886.911, 6020.023, 6156.144, 6295.344, 6437.69, 6583.256, 6732.113, 6884.335, 7040.0, 7199.185, 7361.968, 7528.433, 7698.662, 7872.74, 8050.753, 8232.793, 8418.948, 8609.312, 8803.981, 9003.052, 9206.624, 9414.799, 9627.681, 9845.377, 10067.995, 10295.647, 10528.446, 10766.509, 11009.955, 11258.906, 11513.486, 11773.823, 12040.046, 12312.289, 12590.687, 12875.381, 13166.511, 13464.225, 13768.671, 14080.0]
# G3 op 119
set :offset, 119 # we start in the key of G

# testen akkoordenprogressie

define :speel do |notes_to_play, duration|
  for n in notes_to_play do
    note = hz_to_midi(freqs[get[:offset] + n])
    puts n, note
    play note, sustain: duration, attack: 0, release: 0, amp: 0.6
  end
  sleep duration
end

live_loop :chords do
  use_synth :sine
  speel [-33, -2,  4, 18], 2.328 + 0.579                 # Gb Gb Ad D
  speel [-31,  0,  7, 18], 2.148 + 0.218 + 0.417 + 0.178 # G  G  A# D
  speel [-23,  0,  8, 18], 2.391 + 0.442 + 0.223 + 0.333 # Bb G  Bb D
  speel [-12,  0,  8, 21], 3.408 + 0.247 + 0.348         # D+ G  Bb Eb
  speel [-30,  1, 11, 19], 4                             # G+ G+ B+ D+
  sleep 2
end

live_loop :melody do
  use_synth :saw
  sleep         2.328
  speel [18],   0.579 # D
  speel [0],    2.148 # G
  sleep         0.218
  speel [19],   0.417 # D+
  speel [14],   0.178 # C+
  speel [8],    2.391 # Bb
  sleep         0.442
  speel [16],   0.223 # Db
  speel [26],   0.333 # F
  speel [21],   3.408 # Eb
  speel [38],   0.247 # A#
  speel [36],   0.348 # A
  speel [32],   4     # G+
  sleep 2
end

Side note: it totally pisses me of that Sonic Pi’s send_midi_note_on function only sends integer values, making it totally useless for microtonal music.

Remark on my side note: actually you can use Surge XT with a custom tuning so (integer) midi notes are mapped to micro tones. You are limited to the 0 - 127 range so with 31 notes per octave that are not many octaves so I needed multiple instances of Surge to get the full range. I have a this python script modified to also generate tuning files for Surge but it’s on my other pc.

2 Likes

Love the mood of this, listening on repeat till drift-off :smiley: