# Functions for custom scales

**URL:** <https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637>\
**Category:** Creations & Ideas\
**Created:** [March 12, 2023, 7:30am UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637 "2023-03-12T07:30:12Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![cubicinfinity](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/cubicinfinity/32/2355_2.png) [@cubicinfinity](https://in-thread.sonic-pi.net/u/cubicinfinity)\
**Post date:** [March 12, 2023, 7:30am UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/1 "2023-03-12T07:30:12Z")

</div>

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.

```ruby
# 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.

```ruby
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

```

---

<div class="post-metadata">

**Author:** ![cubicinfinity](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/cubicinfinity/32/2355_2.png) [@cubicinfinity](https://in-thread.sonic-pi.net/u/cubicinfinity)\
**Post date:** [March 12, 2023, 7:31am UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/2 "2023-03-12T07:31:46Z")

</div>

Any feedback for better functions and usage are appreciated.

---

<div class="post-metadata">

**Author:** ![ts-swx](https://avatars.discourse-cdn.com/v4/letter/t/53a042/32.png) [@ts-swx](https://in-thread.sonic-pi.net/u/ts-swx)\
**Post date:** [March 12, 2023, 4:37pm UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/3 "2023-03-12T16:37:01Z")

</div>

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!

---

<div class="post-metadata">

**Author:** ![hitsware](https://avatars.discourse-cdn.com/v4/letter/h/a9adbd/32.png) [@hitsware](https://in-thread.sonic-pi.net/u/hitsware)\
**Post date:** [March 12, 2023, 6:46pm UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/4 "2023-03-12T18:46:56Z")

</div>

```ruby
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
```

---

<div class="post-metadata">

**Author:** ![cubicinfinity](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/cubicinfinity/32/2355_2.png) [@cubicinfinity](https://in-thread.sonic-pi.net/u/cubicinfinity)\
**Post date:** [March 12, 2023, 11:07pm UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/5 "2023-03-12T23:07:40Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![cubicinfinity](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/cubicinfinity/32/2355_2.png) [@cubicinfinity](https://in-thread.sonic-pi.net/u/cubicinfinity)\
**Post date:** [March 12, 2023, 11:12pm UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/6 "2023-03-12T23:12:23Z")

</div>

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.

```ruby
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:

```ruby
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

```

---

<div class="post-metadata">

**Author:** ![hitsware](https://avatars.discourse-cdn.com/v4/letter/h/a9adbd/32.png) [@hitsware](https://in-thread.sonic-pi.net/u/hitsware)\
**Post date:** [March 13, 2023, 12:02am UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/7 "2023-03-13T00:02:31Z")

</div>

```ruby
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
```

---

<div class="post-metadata">

**Author:** ![cubicinfinity](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/cubicinfinity/32/2355_2.png) [@cubicinfinity](https://in-thread.sonic-pi.net/u/cubicinfinity)\
**Post date:** [March 13, 2023, 12:21am UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/8 "2023-03-13T00:21:40Z")

</div>

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.

```ruby
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

```

---

<div class="post-metadata">

**Author:** ![hitsware](https://avatars.discourse-cdn.com/v4/letter/h/a9adbd/32.png) [@hitsware](https://in-thread.sonic-pi.net/u/hitsware)\
**Post date:** [March 13, 2023, 12:52am UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/9 "2023-03-13T00:52:29Z")

</div>

> 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 …

---

<div class="post-metadata">

**Author:** ![binarysweets](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/binarysweets/32/2683_2.png) [@binarysweets](https://in-thread.sonic-pi.net/u/binarysweets)\
**Post date:** [March 13, 2023, 10:08am UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/10 "2023-03-13T10:08:05Z")

</div>

This is great!

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

```ruby
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
```

---

<div class="post-metadata">

**Author:** ![cubicinfinity](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/cubicinfinity/32/2355_2.png) [@cubicinfinity](https://in-thread.sonic-pi.net/u/cubicinfinity)\
**Post date:** [March 13, 2023, 4:41pm UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/11 "2023-03-13T16:41:12Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![cubicinfinity](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/cubicinfinity/32/2355_2.png) [@cubicinfinity](https://in-thread.sonic-pi.net/u/cubicinfinity)\
**Post date:** [March 13, 2023, 4:46pm UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/12 "2023-03-13T16:46:47Z")

</div>

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.)

---

<div class="post-metadata">

**Author:** ![cubicinfinity](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/cubicinfinity/32/2355_2.png) [@cubicinfinity](https://in-thread.sonic-pi.net/u/cubicinfinity)\
**Post date:** [March 14, 2023, 1:19am UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/13 "2023-03-14T01:19:27Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![hitsware](https://avatars.discourse-cdn.com/v4/letter/h/a9adbd/32.png) [@hitsware](https://in-thread.sonic-pi.net/u/hitsware)\
**Post date:** [March 14, 2023, 2:41am UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/14 "2023-03-14T02:41:46Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![binarysweets](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/binarysweets/32/2683_2.png) [@binarysweets](https://in-thread.sonic-pi.net/u/binarysweets)\
**Post date:** [March 14, 2023, 9:17am UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/15 "2023-03-14T09:17:52Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![amiika](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/amiika/32/2401_2.png) [@amiika](https://in-thread.sonic-pi.net/u/amiika)\
**Post date:** [March 17, 2023, 9:11pm UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/16 "2023-03-17T21:11:03Z")

</div>

If you like Scale Workshop, you might like this:

```ruby
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](https://github.com/amiika/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:

```ruby
# 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

```

---

<div class="post-metadata">

**Author:** ![cubicinfinity](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/cubicinfinity/32/2355_2.png) [@cubicinfinity](https://in-thread.sonic-pi.net/u/cubicinfinity)\
**Post date:** [March 17, 2023, 9:32pm UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/17 "2023-03-17T21:32:24Z")

</div>

Directly reading scala files is very nice.

---

<div class="post-metadata">

**Author:** ![amiika](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/amiika/32/2401_2.png) [@amiika](https://in-thread.sonic-pi.net/u/amiika)\
**Post date:** [March 17, 2023, 11:32pm UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/18 "2023-03-17T23:32:19Z")

</div>

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](https://en.xen.wiki/w/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`

(from [xenharmonic-devs/scale-workshop(github.com)](https://github.com/xenharmonic-devs/scale-workshop)

---

<div class="post-metadata">

**Author:** ![ts-swx](https://avatars.discourse-cdn.com/v4/letter/t/53a042/32.png) [@ts-swx](https://in-thread.sonic-pi.net/u/ts-swx)\
**Post date:** [March 20, 2023, 5:57pm UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/19 "2023-03-20T17:57:00Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![cubicinfinity](https://dub1.discourse-cdn.com/flex017/user_avatar/in-thread.sonic-pi.net/cubicinfinity/32/2355_2.png) [@cubicinfinity](https://in-thread.sonic-pi.net/u/cubicinfinity)\
**Post date:** [March 25, 2023, 9:34pm UTC](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637/20 "2023-03-25T21:34:30Z")

</div>

Added `rationalize` function.  
Uploaded to GitHub: [GitHub - CUBICinfinity/sonic\_pi\_resources: Scripts, Samples, Synths, etc. for Sonic Pi](https://github.com/CUBICinfinity/sonic_pi_resources)

[Next page](https://in-thread.sonic-pi.net/t/functions-for-custom-scales/7637.md?page=2)
