Python audio processing at lightspeed ⚡ Part 2: pytuning
Tags: python, audio
Published: 2020-02-03 10:59:48

In the last post, I covered how zignal can make basic kinds of waves and manipulate and display them. Now I am going to describe another audio library called `pytuning`. This library makes scales which can tune synthesizers like yoshimi or zynaddsubfx.

In order for you to understand this post, you must understand what is **tuning**. Tuning is the process of "adjusting the pitch of one or many tones from musical instruments to establish typical intervals between these tones" ([Wikipedia](https://en.wikipedia.org/wiki/Musical_tuning) reference). In other words, each instrument can make tunes of different pitches/frequencies, there are pitches that these tones are supposed to be at depending on the instrument and tuning calibrates the tone to be the correct reference pitch/frequency for the instrument.

There are defined frequencies for each musical note. For example, the note A above middle C (A4) is fixed to 440Hz, and the way most people tune their instruments is by listening to the pitch that the tone makes and guess if it's the correct frequency. It takes a trained ear to get used to each frequency. There are also electronic tuners which can calculate the pitch that is played.

## How pytuning is useful

While `pytuning` is not a replacement for a tuner, it has different scales of frequencies that you can create and tweak to sound consonant. An untuned or improperly tuned scale, and by extension instrument, will have some out of tune notes and such notes are called **dissonant** and there is dissonance in the scale.

To give an example, [Just intonation](https://en.wikipedia.org/wiki/Just_intonation) uses ratio intervals like 3/2 and then normalizes the scale. It sounds pleasant but as a consequence of the fractional interval it has some dissonant notes. In order to make a consonant scale you would use even temperament (also called EDO) scales which means the intervals between two notes that are next to each other are identical. Scales will be defined in the next section.

The goal of pytuning is to allow musicians who use uncommon scales (any scale that's not EDO) to make them sound as consonant as possible, although this library can also make and work with EDO scales just fine.

**Degrees** are ratios of the frequency of a pitch and that of a reference pitch. A degree describes a note like B4 between the scale A4 and A5. An **interval** is the length of the scale, or how far equivalent degrees in adjacent scales are e.g. the interval of the scale A5-A4 is the equal to the interval of scale B5-B4.

Among the intervals supported are:

* octave: the interval is 2
* perfect fifth: 3/2
* minor second tet: `sqrt(12)`
* lucy long scale: `2**(1/(2*pi))`

## Scales

**Scales** are a list of degrees. The list always begins with a **unison** (1) and ends with a **octave** (2). The octave can actually be any formal number but in this post I will always assume the octave is 2, and most of the scale generating functions assume a default octave of 2 as well.

*With this interval, the next degrees of a scale have twice as much frequency as the previous scale*. As a concrete example, each semitone of piano keys, which have octave intervals, is just a scale, and the next highest semitone has twice as much frequency.

Here is a non-exhaustive list of scales that are available:

- Harmonic - `create_harmonic_scale(first, last, normalize=True)`. All the degrees in this scale are harmonics of the sequence `first/first`, `(first+1)/first`, ..., `last/first`.
- EDO (Equal Division of the Octave, or Equal Temperament) - `create_edo_scale(number_tones)`. This is the prevalent scale used in most music, especially the 12 degree EDO (12-EDO). `number_tones` does not count the unison. EDO scales are always normalized.
- Pythagorean - `create_pythagorean_scale(scale_size=12, number_down_fifths=6)`. There are an enormous number of intervals for this scale, but it suffices to know that the default arguments use a perfect fifth (3/2) as the interval.
- Quarter-Comma Meantone - `create_quarter_comma_meantone_scale(scale_size=12, number_down_fifths=6)`. Almost identical to Pythagorean scales except the interval is `perfect_fifth/pow(81/80, 1/4)`.

There are also functions to create Euler-Fokker Genera scales and Diatonic scales.

Some scales can be normalized which just means *"finding the smallest power of two (or octave) that when multiplied by the interval (in the case of an interval less than 1) or divided into the interval (for intervals greater than 2) will bring the interval into the target range of `1 <= i <= 2`."* ([Source](https://pytuning.readthedocs.io/en/0.7.2/utilities.html#pytuning.utilities.normalize_interval))

So, to normalize the number 9, it would have to be divided by 8 to make a number less than 2, the octave. So all of the numbers in the interval would need to be divided by 8, and then sandwiched between the unison and the octave in a list.

A **mode** is a mask of the scale where different degrees are selectively chosen. Taken from the documentation, you can filter the degrees of a 12-EDO:

```python
major_mask = (0,2,4,5,7,9,11,12)
major_mode = mask_scale(create_edo_scale(12), major_mask)
```

This produces:

![12-EDO math result](https://raw.githubusercontent.com/ZenulAbidin/drawio/master/edo_scale.jpg)
## Tuning tables

Eventually the scale degrees have to be converted to frequencies. The way this is done depends on the scale, but for the 12-EDO scale there are MIDI keys which map to frequencies.

Note: In this table, octave is used to sort the scale numbers by frequency, assuming each scale has an interval of 2.

Octave|  C|  C#|   D|  D#|   E|   F|  F#|   G|  G#|   A|  A#|   B
------|---| ---| ---| ---| ---| ---| ---| ---| ---| ---| ---| ---
-2 | 0 |   1 |   2 |   3 |   4 |   5 |   6 |   7 |   8 |   9 |  10 |  11
-1 |12 |  13 |  14 |  15 |  16 |  17 |  18 |  19 |  20 |  21 |  22 |  23
 0 |24 |  25 |  26 |  27 |  28 |  29 |  30 |  31 |  32 |  33 |  34 |  35
 1 |36 |  37 |  38 |  39 |  40 |  41 |  42 |  43 |  44 |  45 |  46 |  47
 2 |48 |  49 |  50 |  51 |  52 |  53 |  54 |  55 |  56 |  57 |  58 |  59
 3 |60 |  61 |  62 |  63 |  64 |  65 |  66 |  67 |  68 |  69 |  70 |  71
 4 |72 |  73 |  74 |  75 |  76 |  77 |  78 |  79 |  80 |  81 |  82 |  83
 5 |84 |  85 |  86 |  87 |  88 |  89 |  90 |  91 |  92 |  93 |  94 |  95
 6 |96 |  97 |  98 |  99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107
 7 |108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |119
 8 |120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | | | 

A MIDI key can be used as a reference note for a scale, in which case the frequencies of the degrees in a scale are relative to the reference MIDI key.


It's also possible to dump the scale into a (python) string called a tuning table. The tuning table can then be read by other synthesizers. Here is how you would make one for scala, yoshimi or zynaddsubfx:

```python
pythag_scale       = create_pythagorean_scale()
scala_tuning_table = create_scala_tuning(pythag_scale, "Pythagorean Scale")
print(scala_tuning_table)
```

This will print:

```
! Scale produced by pytuning. For tuning yoshimi or zynaddsubfx,
! only include the portion below the final '!'
!
Pythagorean Scale
 12
!
256/243
9/8
32/27
81/64
4/3
1024/729
3/2
128/81
27/16
16/9
243/128
2/1
```

Timidity++ tuning tables can also be made by pytuning. Here each entry is 1000 times the actual frequency, which is a format that Timidity can understand:

```python
reference_note = 60
scale = create_euler_fokker_scale([3,5],[3,1])

tuning = create_timidity_tuning(scale, reference_note=reference_note)
print(tuning)
```

These are the first few lines of output:

```
# Timidity tuning table created by pytuning,
# call timidity with the -Z option to enable.
# Note reference: 60; Freq reference: 261.625565 Hz
1437
1533
1725
1916
2044
2156
2299
2555
2874
3066
... More entries below
```

Similarly there are functions which create tuning tables for fluidsynth and Csound.

## Metric functions

Metric functions compute metrics related to a scale. One metric will be covered here and it's the **consonance**. Basically it is how repetitive the sound is, as in repeated consonants in a sentence like *"She ate seven sandwiches on a sunny Sunday last year"* except with frequencies instead of letters of course.

In pytuning, the consonance is calculated by typing:

```python
pythag = create_pythagorean_scale()
metric = sum_p_q_for_all_intervals(pythag)
```

The `metric` variable will contain `{'sum_p_q_for_all_intervals': 1092732}`. A smaller value means a higher consonance.

## Under the hood

It is possible to use pytuning like a normal pip package, but in order to fully appreciate the library, you need to install its dependencies using `pip install matplotlib scikit-learn PyQt5 jupyter`. Jupyter comes with a truckload of dependencies so downloading could take a while. Make sure you also have [Qt5](https://www.qt.io/download) installed, use the open source option. This might be available in your package manager.

When all that is done, the normal Jupyter commands will be available. If you aren't familiar with Jupyter, like me I admit, the only thing you need to know is that there is a program called "jupyter-qtconsole" which you can run. This command opens a graphical window that contains an IPython console, a variant of the Python console.

When Pytuning is installed, it will put a script called `interactive.py` in the `bin/` folder, relative to wherever Python is installed in. I say this because you might have installed pytuning in a virtualenv so in that case `interactive.py` will be in the virtualenv's `bin/` folder.

`interactive.py` is a helper script that imports all of the pytuning functions and also some other math libraries like sympy and matplotlib. When you open jupyter-qtconsole, you should type `%load path-to-interactive.py` in order to run this script inside the console. Then you confirm that you want to run it by pressing Shift-Enter.

Jupyter-qtconsole can make graphics and formatted math like this:

![jupyter qtconsole math window](https://pytuning.readthedocs.io/en/0.7.2/_images/rich_printing.png)
![jupyter qtconsole plot window](https://pytuning.readthedocs.io/en/0.7.2/_images/graphics.png)

## Closing words

I am very thankful for the developer(s) of this library for writing  [readthedocs documentation](https://pytuning.readthedocs.io/en/0.7.2/index.html) for this library because it made writing this post so much easier. Documentation and commented functions are important for any serious library because not only does it help users make use of this library, it also helps people like me blog about it. And as I like to say, **source code is not documentation!!!**

Got any python audio libraries you want me to talk about? Let me know in the comments so I can write about them too, but I'm more likely to write about it if it has documentation.

And it goes without saying, if you see any errors in this post, let me know so I can correct them.

Image by <a href="https://pixabay.com/users/GDJ-1086657/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=2099064">Gordon Johnson</a> from <a href="https://pixabay.com/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=2099064">Pixabay</a>