Constructing musical tunings with biosignals#

This notebook is intended to demonstrate the use of the biotuner for tuning construction. To do so, we will explore how different tuning construction methods developed in the field of music theory can be integrated with electrophysiological signal processing techniques.

The terms ‘’tuning’’ and ‘’scale’’ will be used interchangeably, referring to a series of ratios that subdivides an octave.

1. Spectral peaks extraction#

Load dataset#

import warnings
warnings.filterwarnings('ignore', category=DeprecationWarning)
import matplotlib.pyplot as plt
import pytuning.visualizations.scales
import numpy as np

data = np.load('../data/EEG_example.npy')

Extract spectral peaks#

The first step is to initialize the biotuner object by calling compute_biotuner and then applying the method peaks_extraction.

The peaks_function argument determines how spectral peaks are extracted. Here the ‘fixed’ method is used, where predetermined frequency bands are used to find one spectral peak associated with each band.

There is no minimum size for time series. However, keep in mind that the minimum frequency that can be observed using spectral decomposition is dependant on the length of your time series. For example, if the sampling frequency equals 1000Hz, time series shorter than 1 second will not allow for peak extraction below 2Hz. Also, note that the precision argument influences the size of the fft and higher precision requires longer time series (e.g. 0.1Hz of precision at 1000Hz of sampling frequency requires 10 seconds of signal).

import time
from biotuner.biotuner_object import compute_biotuner
# Define frequency bands for peaks_function = 'fixed'
FREQ_BANDS = [[1, 3], [3, 7], [7, 12], [12, 18], [18, 30], [30, 45]] 

# Select a single time series
data_ = data[28]
start = time.time()

# Initialize biotuner object
biotuning = compute_biotuner(sf = 1000, peaks_function = 'fixed', precision = 0.5, n_harm = 10)

# Extract spectral peaks
biotuning.peaks_extraction(data_, FREQ_BANDS = FREQ_BANDS, ratios_extension = True, max_freq = 30, n_peaks=5,
                          graph=False, min_harms=2)

stop = time.time()
print(stop-start)
0.001957416534423828
# Print the extracted peaks
biotuning.peaks
array([ 1.5,  5. ,  8. , 13. , 19. , 30.5])

Now that we have shown how spectral peaks can be extracted, we will use these peaks as the basis for the organisation of musical structures. We will show how to derive tunings based on the harmonicity of spectral peaks, expressed as the simplicity of their frequency ratios.

2. Deriving tuning from peaks ratios#

The simpliest way to derive a tuning from spectral peaks is to use their ratios as steps for dividing the octave. An attribute of the biotuner object provides this information.

biotuning.peaks_ratios 
[1.0833333333333333,
 1.1730769230769231,
 1.1875,
 1.2708333333333333,
 1.3,
 1.3333333333333333,
 1.4615384615384615,
 1.525,
 1.5833333333333333,
 1.6,
 1.605263157894737,
 1.625,
 1.6666666666666667,
 1.9,
 1.90625]

You can also derive a tuning from extended peaks ratios as well as from extended peaks ratios with higher consonance levels. Extended peaks correspond to a set of frequencies based on the harmonic congruence of spectral peaks.

biotuning.peaks_extension(method = 'harmonic_fit', harm_function = 'mult',  n_harm = 20, 
                          cons_limit = 0.129, ratios_extension = True, scale_cons_limit = 0.129) 
print(np.sort(biotuning.extended_peaks_ratios))
Number of extended peaks :  16
[1.01 1.02 1.03 1.05 1.07 1.08 1.09 1.15 1.17 1.19 1.2  1.23 1.25 1.26
 1.27 1.29 1.3  1.31 1.33 1.35 1.37 1.46 1.48 1.5  1.52 1.54 1.56 1.58
 1.6  1.61 1.62 1.67 1.7  1.71 1.73 1.83 1.85 1.88 1.9  1.91 1.93 1.98
 2.  ]

Since the tuning from extended peaks might have a large number of steps, we could keep only the most consonant ratios. To do so, we adjust the ‘scale_cons_limit’ value:

Comparisons with familiar ratios:

    #Unison-frequency ratio 1:1 yields a value of 2
    Octave-frequency ratio 2:1 yields a value of 1.5
    Perfect 5th-frequency ratio 3:2 yields a value of 0.833
    Perfect 4th-frequency ratio 4:3 yields a value of 0.583
    Major 6th-frequency ratio 5:3 yields a value of 0.533
    Major 3rd-frequency ratio 5:4 yields a value of 0.45
    Minor 3rd-frequency ratio 5:6 yields a value of 0.366
    Minor 6th-frequency ratio 5:8 yields a value of 0.325
    Major 2nd-frequency ratio 8:9 yields a value of 0.236
    Major 7th-frequency ratio 8:15 yields a value of 0.192
    Minor 7th-frequency ratio 9:16 yields a value of 0.174
    Minor 2nd-frequency ratio 15:16 yields a value of 0.129
biotuning.peaks_extension(method = 'harmonic_fit', harm_function = 'mult',  n_harm = 30, 
                          cons_limit = 0.1, ratios_extension = True, scale_cons_limit = 0.129) 
print(biotuning.extended_peaks_ratios_cons)

biotuning.peaks_extension(method = 'harmonic_fit', harm_function = 'mult',  n_harm = 30, 
                          cons_limit = 0.1, ratios_extension = True, scale_cons_limit = 0.236) 
print(biotuning.extended_peaks_ratios_cons)
Number of extended peaks :  21
[1.083 1.125 1.154 1.2   1.231 1.25  1.3   1.333 1.444 1.462 1.5   1.583
 1.6   1.625 1.667 1.778 1.875 1.9   2.   ]
Number of extended peaks :  21
[1.125 1.2   1.25  1.333 1.5   1.6   1.667 2.   ]

3. Dissonance curve#

The dissonance curve has been introduced by William Sethares showing how the harmonic structure that constitutes a timbre can be reflected in scale construction, allowing to find tunings that will match the timbral structure. I strongly recommend reading his book Tuning, Timbre, Spectrum and Scale.

The dissonance curve takess as input list of peaks and their associated amplitudes. Since providing less than 6 spectral peaks would lead to less interesting dissonance curve, the method ‘’peaks_extension’’ provides a way to extend the number of peaks based on the harmonic_fit function (see biotuner.py)

peaks_extension methods: [‘harmonic_fit’, ‘consonant’, ‘multi_consonant’, ‘consonant_harmonic_fit’, ‘multi_consonant_harmonic_fit’]

# Extend spectral peaks based on their inter-harmonic concordance
biotuning.peaks_extension(method = 'harmonic_fit', harm_function = 'mult',  n_harm = 20, cons_limit = 0.05, 
                          ratios_extension = True, scale_cons_limit = 0.1)
# Compute the dissonance curve
biotuning.compute_diss_curve(plot = True, input_type = 'extended_peaks', euler_comp = False, denom = 100, max_ratio = 2, n_tet_grid = 12)
Number of extended peaks :  16
../../_images/scale_construction_17_1.png
print('Dissonance curve metrics:', biotuning.scale_metrics)
print('Dissonance curve tuning:', biotuning.diss_scale)
print('Dissonance curve consonant tuning:', biotuning.diss_scale_cons)
Dissonance curve metrics: {'diss_euler': 'NaN', 'dissonance': 5.9114886117207055, 'diss_harm_sim': 14.059883388112603, 'diss_n_steps': 18}
Dissonance curve tuning: [1.0952380952380953, 1.1538461538461537, 1.1868131868131868, 1.2307692307692308, 1.25, 1.2659574468085106, 1.3, 1.3111111111111111, 1.3333333333333333, 1.3970588235294117, 1.4606741573033708, 1.6, 1.6236559139784945, 1.6666666666666667, 1.8450704225352113, 1.8736842105263158, 1.898989898989899, 2.0]
Dissonance curve consonant tuning: [1.154 1.231 1.25  1.3   1.333 1.6   1.667 2.   ]
from biotuner.biotuner_utils import scale2frac

### If you want the tuning in fraction
scale_frac, num, denom = scale2frac(biotuning.diss_scale, maxdenom = 1000)
scale_frac
[23/21,
 15/13,
 108/91,
 16/13,
 5/4,
 119/94,
 13/10,
 59/45,
 4/3,
 95/68,
 130/89,
 8/5,
 151/93,
 5/3,
 131/71,
 178/95,
 188/99,
 2]

4. Harmonic entropy#

Harmonic entropy is a simple model to quantify the extent to which musical chords exhibit various psychoacoustic effects, lumped together in a single construct called psychoacoustic concordance.

The compute_harmonic_entropy method takes as input a list of ratios.

If input_type is set to ‘peaks’ or ‘extended_peaks’, their ratios will be used. Other input types use extended ratios : see biotuner ref.

‘extended_ratios_inc’ (increments of the ratios in the form of r^1, r^2, r^3, … r^n)

‘extended_ratios_inc_fit’ (harmonic fit between the ratios increments)

‘extended_ratios_harm’ (harmonics of the ratios in the form rx1, rx2, rx3, … rxn)

# Initialize biotuner object, specifying that you want ratios increments and harmonics to be computed
biotuning = compute_biotuner(sf = 1000, peaks_function = 'harmonic_recurrence', precision = 0.5, n_harm = 30,
                             ratios_inc=True, ratios_harms=True, ratios_inc_fit=True)
# Extract spectral peaks
biotuning.peaks_extraction(data_, FREQ_BANDS = FREQ_BANDS, ratios_extension = True, max_freq = 30, n_peaks=5,
                          graph=False, min_harms=2)
# compute the extended peaks by finding inter-harmonic concordance.
biotuning.peaks_extension(method = 'harmonic_fit', harm_function = 'mult',  n_harm = 30, 
                          cons_limit = 0.1, ratios_extension = True)
# compute the harmonic entropy curve as a function of extended ratios increments.
biotuning.compute_harmonic_entropy(input_type = 'extended_ratios_inc', plot_entropy = True, octave = 2, rebound = False, 
                                   scale_cons_limit = 0.01, sub=False)

# compute the harmonic entropy curve as a function of extended ratios harmonics.
biotuning.compute_harmonic_entropy(input_type = 'extended_ratios_harms', plot_entropy = True, octave = 2, rebound = False, 
                                   scale_cons_limit = 0.01, sub=False)
Number of extended peaks :  20
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[10], line 11
      8 biotuning.peaks_extension(method = 'harmonic_fit', harm_function = 'mult',  n_harm = 30, 
      9                           cons_limit = 0.1, ratios_extension = True)
     10 # compute the harmonic entropy curve as a function of extended ratios increments.
---> 11 biotuning.compute_harmonic_entropy(input_type = 'extended_ratios_inc', plot_entropy = True, octave = 2, rebound = False, 
     12                                    scale_cons_limit = 0.01, sub=False)
     14 # compute the harmonic entropy curve as a function of extended ratios harmonics.
     15 biotuning.compute_harmonic_entropy(input_type = 'extended_ratios_harms', plot_entropy = True, octave = 2, rebound = False, 
     16                                    scale_cons_limit = 0.01, sub=False)

File ~/git/biotuner/biotuner/biotuner_object.py:996, in compute_biotuner.compute_harmonic_entropy(self, input_type, res, spread, plot_entropy, plot_tenney, octave, rebound, sub, scale_cons_limit)
    993 if scale_cons_limit is None:
    994     scale_cons_limit = self.scale_cons_limit
--> 996 HE_scale, HE = harmonic_entropy(
    997     ratios,
    998     res=res,
    999     spread=spread,
   1000     plot_entropy=plot_entropy,
   1001     plot_tenney=plot_tenney,
   1002     octave=octave,
   1003 )
   1004 self.HE_scale = HE_scale[0]
   1005 self.HE_scale_cons, b = consonant_ratios(
   1006     self.HE_scale, scale_cons_limit, sub=False, input_type="ratios"
   1007 )

File ~/git/biotuner/biotuner/scale_construction.py:704, in harmonic_entropy(ratios, res, spread, plot_entropy, plot_tenney, octave)
    702 ratios = numerators / denominators
    703 bendetti_heights = numerators * denominators
--> 704 tenney_heights = log2(bendetti_heights)
    706 ind = np.argsort(tenney_heights)  # sort by Tenney height
    707 bendetti_heights = bendetti_heights[ind]

TypeError: only size-1 arrays can be converted to Python scalars
print('Harmonic entropy metrics:', biotuning.scale_metrics)
print('Harmonic entropy tuning:', biotuning.HE_scale)
print('Harmonic entropy consonant tuning:', biotuning.HE_scale_cons)
Harmonic entropy metrics: {'HE': 1.2165484571397156, 'HE_n_steps': 21, 'HE_harm_sim': 0.5818470682340359}
Harmonic entropy tuning: [1.018 1.036 1.07  1.101 1.122 1.196 1.266 1.283 1.315 1.349 1.379 1.451
 1.494 1.57  1.603 1.646 1.71  1.792 1.818 1.927 1.967]
Harmonic entropy consonant tuning: [1.07  1.57  1.71  1.792]
# If you want the tuning in fraction
scale_frac, num, denom = scale2frac (biotuning.HE_scale, maxdenom = 100)
print(scale_frac)
[57/56, 86/83, 107/100, 109/99, 46/41, 61/51, 119/94, 68/53, 96/73, 58/43, 131/95, 74/51, 124/83, 157/100, 109/68, 107/65, 171/100, 138/77, 20/11, 185/96, 179/91]

5. Classical tuning creation#

We will see in this section how to use spectral peaks and their ratios to derive tunings based on Euler-Fokker Genera, Harmonic positions and Generator Intervals

5.1. Euler-Fokker Genera#

Euler-Fokker genera are musical scale in just intonation whose pitches can be expressed as products of some of the members of some multiset of generating prime factors.

# Initialize biotuner object
BT_EFG = compute_biotuner(sf = 1000, peaks_function = 'harmonic_recurrence', precision = 0.5,
                                          n_harm = 15, ratios_n_harms = 5, ratios_inc_fit = True,
                                          ratios_inc = True)

# Extract spectral peaks
BT_EFG.peaks_extraction(data_, ratios_extension = True, max_freq = 50)

# Use peaks as generators (using method)
print('Generators :', BT_EFG.peaks)
BT_EFG.euler_fokker_scale()
Number of peaks : 5
Generators : [ 2.   6.5  8.   3.5 11.5]
[1, 33/32, 11/8, 3/2, 2]
from biotuner.scale_construction import euler_fokker_scale
from biotuner.biotuner_utils import scale_interval_names

# compute extended peaks
BT_EFG.peaks_extension(method = 'harmonic_fit', harm_function = 'mult', 
                                       n_harm = 10, cons_limit = 0.1, ratios_extension = True)

# Use extended peaks as generators (using function)
EFG_scale = euler_fokker_scale(BT_EFG.extended_peaks)

# print the scale steps with associated names
scale_interval_names(EFG_scale, reduce = False)
Number of extended peaks :  8
[[1, 'Unison'],
 [33/32, 'Thrty-third Harmonic'],
 [9/8, 'Pythagorean Major Second'],
 [77/64, 'Seventy-seventh Harmonic'],
 [21/16, '21st Harmonic'],
 [693/512, None],
 [11/8, 'Eleventh Harmonic'],
 [3/2, 'Perfect Fifth'],
 [99/64, 'Ninety-ninth Harmonic'],
 [7/4, 'Septimal Minor Seventh'],
 [231/128, None],
 [63/32, 'Sixty-third Harmonic'],
 [2, 'Octave']]

5.2. Harmonic scale#

Harmonic scales are constructed by transforming a set of harmonic positions into ratios falling between the unison (1) and the octave (2). For example, using the harmonics 3, 5 and 7 would yields a scale of 1, 1.5, 1.25 and 1.75, since 3/2=1.5, 5/2^2 = 1.25 and 7/2^2 = 1.75

5.2.1. Using harmonic recurrence#

The harmonic recurrence method computes all peaks of the spectrum, and keeps peaks for which a maximum of other peaks are harmonics. This method gives as an ouput the peaks and the positions of congruent harmonics. The max_freq argument determines the maximum frequency that can be choose as a peak. The min_harms argument indicates the minimum number of congruent harmonics to keep the peak.

# Initialize biotuner object
biotuning_harm_peaks = compute_biotuner(sf = 1000, peaks_function = 'harmonic_recurrence', precision = 0.5) 

# Extract spectral peaks with minimum of 2 recurrent harmonics
biotuning_harm_peaks.peaks_extraction(data_, min_freq = 5, max_freq = 20, min_harms = 2, harm_limit = 128)
print(biotuning_harm_peaks.all_harmonics)

# Extract spectral peaks with minimum of 4 recurrent harmonics
biotuning_harm_peaks.peaks_extraction(data_, min_freq = 5, max_freq = 20, min_harms = 4, harm_limit = 128)
print(biotuning_harm_peaks.all_harmonics)
Number of peaks : 5
[ 2.  3.  5.  6.  7.  8.  9. 10. 12. 14. 18. 19. 20. 21. 23. 30. 31. 37.
 41. 42. 45. 52. 60. 62. 66. 67. 71. 73.]
Number of peaks : 3
[ 2.  5.  6.  7.  8.  9. 10. 12. 18. 19. 20. 21. 30. 31. 42. 45. 52. 60.
 62. 66. 67. 71. 73.]
from biotuner.scale_construction import harmonic_tuning
# deriving harmonic tuning from recurrent harmonics
harm_tuning = harmonic_tuning(biotuning_harm_peaks.all_harmonics)
print(harm_tuning)
[1.03125, 1.046875, 1.109375, 1.125, 1.140625, 1.1875, 1.25, 1.3125, 1.40625, 1.5, 1.625, 1.75, 1.875, 1.9375, 2.0]

Instead of using all the harmonics, we can use the harmonic positions of a single peak to generate a tuning

# find harmonic positions of first peak
harmonics = biotuning_harm_peaks.harm_peaks_fit[0][1]

# compute harmonic tuning
harm_peak_tuning = harmonic_tuning(harmonics)

# print results
print('Harmonics', [int(x) for x in harmonics])
print('Tuning', harm_peak_tuning)
Harmonics [5, 7, 8, 19, 42, 45, 52, 62, 66, 67, 71, 73]
Tuning [1.03125, 1.046875, 1.109375, 1.140625, 1.1875, 1.25, 1.3125, 1.40625, 1.625, 1.75, 1.9375, 2.0]

We will now compute the harmonicity metrics associated with the tuning

from biotuner.metrics import tuning_to_metrics
metrics = tuning_to_metrics(harm_peak_tuning)
metrics
c:\Users\User\anaconda3\envs\biotuner\lib\site-packages\numpy\lib\function_base.py:380: RuntimeWarning: Mean of empty slice.
  avg = a.mean(axis)
c:\Users\User\anaconda3\envs\biotuner\lib\site-packages\numpy\core\_methods.py:189: RuntimeWarning: invalid value encountered in double_scalars
  ret = ret.dtype.type(ret / rcount)
{'sum_p_q': 708,
 'sum_distinct_intervals': 132,
 'metric_3': 89.5821733821734,
 'sum_p_q_for_all_intervals': 12718,
 'sum_q_for_all_intervals': 5238,
 'harm_sim': 20.53,
 'matrix_harm_sim': 7.408307919171108,
 'matrix_cons': 0.07770664568642607,
 'matrix_denom': 44.07575757575758}

5.2.2. Using inter-harmonic concordance#

This method constructs the harmonic tuning based on the positions of common harmonics of spectral peaks.

# Initialize biotuner object
bt_harm_fit_peaks = compute_biotuner(sf = 1000, peaks_function = 'harmonic_recurrence', precision = 0.5) 

# Extract spectral peaks with minimum of 2 recurrent harmonics
bt_harm_fit_peaks.peaks_extraction(data_, min_freq = 5, max_freq = 20, min_harms = 2, harm_limit = 128)
print(biotuning_harm_peaks.all_harmonics)

# Compute the harmonic tuning
harm_tuning2 = bt_harm_fit_peaks.harmonic_fit_tuning(n_harm =128, bounds = 0.1, n_common_harms = 50)
print(harm_tuning2)
Number of peaks : 5
[ 2.  5.  6.  7.  8.  9. 10. 12. 18. 19. 20. 21. 30. 31. 42. 45. 52. 60.
 62. 66. 67. 71. 73.]
[1.015625, 1.078125, 1.15625, 1.21875, 1.25, 1.4375, 1.5, 1.625, 1.734375, 1.75, 1.796875, 2.0]
# Compute harmonicity metrics associated with the tuning
metrics = tuning_to_metrics(harm_tuning2)
metrics
{'sum_p_q': 844,
 'sum_distinct_intervals': 100,
 'metric_3': 102.607080278920,
 'sum_p_q_for_all_intervals': 11938,
 'sum_q_for_all_intervals': 4922,
 'harm_sim': 24.52,
 'matrix_harm_sim': 12.174391250478207,
 'matrix_cons': 0.13362222995375167,
 'matrix_denom': 42.43939393939394}

5.3. Scale from generator interval#

To derive a scale from a generator interval, the first step is to find which interval will be used based on the harmonic structure of the time series. In this example, we will find the most consonant peaks ratio and use it as a generator interval for tuning construction.

from biotuner.metrics import consonant_ratios
# Define data (single time series)
data_gen = data[40] 

# Initialize biotuner object
bt_gen_int = compute_biotuner(sf = 1000, peaks_function = 'EMD', precision = 0.1, n_harm = 10,
                    ratios_n_harms = 5, ratios_inc_fit = True, ratios_inc = True) # Initialize biotuner object

# Extract spectral peaks
bt_gen_int.peaks_extraction(data_gen, FREQ_BANDS = FREQ_BANDS, ratios_extension = True, max_freq = 50)

# Find most consonant peaks ratio
ratios, cons = consonant_ratios(bt_gen_int.peaks, limit = 0.01, input_type = 'peaks', metric = 'harmsim') #finding the intervals
ratio_arg = np.argmax(cons)
cons_ratio = ratios[ratio_arg]
Index_max: all zeros indicate 1/f trend [0, 33, 46, 20, 93, 20]
Number of peaks : 5
c:\Users\User\anaconda3\envs\biotuner\lib\site-packages\scipy\signal\_spectral_py.py:1999: UserWarning: nperseg = 10000 is greater than input length  = 9501, using nperseg = 9501
  warnings.warn('nperseg = {0:d} is greater than input length '

The most simple way to derive a scale from a generator interval is to generate an equal temperament tuning, meaning that each step equally spaced on a logarithmic scale. To do so, we take our consonant ratio, and find in how many steps we need to divide the octave to retrieve this ratio within the steps. Then, we generate the NTET with this number of steps.

from biotuner.biotuner_utils import NTET_ratios, ratio2frac
from biotuner.scale_construction import oct_subdiv

# Find potential number of steps to divide the octave
octdiv, _ = oct_subdiv(cons_ratio)
print('Consonant ratio :', ratio2frac(cons_ratio, maxdenom=100), '\nPossible octave subdivisions :', octdiv)

# Construct the NTET
NTET = NTET_ratios(octdiv[0], max_ratio=2)
Consonant ratio : [102, 83] 
Possible octave subdivisions : [37, 74, 121, 158, 195]

The biotuner also provides a function that looks at multiple consonant peaks ratios and find the number of subdivisions that optimize having multiple consonant ratios within the same NTET scale.

from biotuner.scale_construction import multi_oct_subdiv

# Find optimal NTET from a series of spectral peaks
oct_divs, x = multi_oct_subdiv(
                bt_gen_int.peaks, max_sub=100,
                octave_limit=0.01365,
                octave=2,
                n_scales=10,
                cons_limit=0.01
            )
print('Optimal NTET divides the octave in {} steps'.format(oct_divs[0]))
Optimal NTET divides the octave in 55 steps

Another way to create a tuning from a generator interval is by using the “create_equal_interval_scale” from PyTuning toolbox. In the creation of such scale, the generator interval can either be used directly (making each successive tone a generator interval above the previous tone), or in an inverted sense (making each interval a generator down from the previous). This function starts from the unison and walks down the number specified, walking up for the rest of the intervals.

# Generate the tuning
gen_interval_tuning = pytuning.scales.create_equal_interval_scale(ratios[0], scale_size = 11, number_down_intervals = 6, octave = 2)
gen_interval_tuning = [float(x) for x in gen_interval_tuning]

# Transform the ratios into fractions with controlled maximum denominator.
frac, _, _ = scale2frac(gen_interval_tuning, 64)
# Using smaller denominator limit will maximize the chance to find intervalic names in the dictionary
frac2, _, _ = scale2frac(gen_interval_tuning, 16)

print(scale_interval_names(frac, reduce = False),'\n\n', scale_interval_names(frac2, reduce = False))
[[1, 'Unison'], [61/58, None], [69/62, None], [48/41, None], [57/46, None], [73/56, None], [51/37, None], [74/51, None], [89/58, None], [92/57, None], [115/64, 'Hundred-fifteenth Harmonic'], [2, 'Octave']] 

 [[1, 'Unison'], [17/16, 'Minor Diatonic Semitone'], [10/9, 'Small Just Whole Tone'], [7/6, 'Septimal Minor Third'], [16/13, 'Tridecimal Neutral Third'], [13/10, 'Tridecimal Major Third '], [11/8, 'Eleventh Harmonic'], [16/11, 'Undecimal Semi-diminished Fifth'], [23/15, None], [21/13, None], [9/5, 'Greater Just Minor Seventh'], [2, 'Octave']]

6. Scale reduction#

When a scale has high number of steps, it might be useful to reduce it to a specific number of steps.

from biotuner.scale_construction import tuning_reduction

# scale reduction based on harmonicity between ratios
tuning_metric, reducted_tuning, mode_metric = tuning_reduction(biotuning.peaks_ratios, mode_n_steps = 4, 
                                                            function = dyad_similarity, ratio_type='pos_harm')
print('Tuning :', np.sort(np.round(biotuning.peaks_ratios, 2)),
      '\nTuning harmonicity :', np.round(tuning_metric, 2),
      '\nMode :', np.sort(reducted_tuning),
      '\nMode harmonicity :', np.round(mode_metric))
Tuning : [1.14 1.23 1.44 1.62 1.64 1.75 1.77 1.86 2.  ] 
Tuning harmonicity : 9.29 
Mode : [1.1429 1.75   1.8571 2.    ] 
Mode harmonicity : 13.0

7. Scale from multiple time series#

Multiple dissonance curves#

Multiple time series can be used to compute multiple dissonance curves. The function diss_curve_multi takes a list of list of frequency peaks and amplitudes and gives as output the dissonant minima that are shared by minimally 2 dissonance curves. Here we iterate across n trials to generate multiple dissonance curves. The same process can be done across electrodes to find tunings that are consistent with the harmonic architecture of multiple electrodes.

from biotuner.biotuner2d import diss_curve_multi
n_harm = 15
peaks_tot = []
amps_tot = []
for d in range(5):
    d +=0
    data_ = data[d]
    diss_tun = compute_biotuner(sf = 1000, peaks_function = 'EMD', precision = 0.5,
                        ratios_n_harms = 5, ratios_inc_fit = True, ratios_inc = True) # Initialize biotuner object
    diss_tun.peaks_extraction(data_, ratios_extension = True, max_freq = 50)
    _ = diss_tun.peaks_extension(method = 'harmonic_fit', n_harm = n_harm)
    peaks_tot.append(diss_tun.extended_peaks)
    amps_tot.append(diss_tun.extended_amps)

diss_tot = diss_curve_multi(peaks_tot, amps_tot, denom=100, max_ratio=2, bound=0.5,
                            labels=['0', '1', '2', '3', '4'], n_tet_grid=12, data_type='Trials')
#biotuning.psd.shape
Index_max: all zeros indicate 1/f trend [2, 2, 11, 9, 10]
Number of peaks : 5
Number of extended peaks :  7
Index_max: all zeros indicate 1/f trend [2, 2, 11, 6, 10]
Number of peaks : 5
Number of extended peaks :  6
Index_max: all zeros indicate 1/f trend [1, 2, 0, 6, 10]
Number of peaks : 5
Number of extended peaks :  14
Index_max: all zeros indicate 1/f trend [2, 6, 1, 5, 10]
Number of peaks : 5
Number of extended peaks :  17
Index_max: all zeros indicate 1/f trend [2, 1, 11, 6, 12]
Number of peaks : 5
Number of extended peaks :  15
6/5
6/5
5/4
5/4
115/88
4/3
4/3
3/2
8/5
5/3
5/3
7/4
174/95
../../_images/scale_construction_54_1.png

8. Exporting scale in scala format#

from biotuner.biotuner_utils import create_SCL
create_SCL(biotuning.HE_scale, 'Harmonic_entropy_scale')
create_SCL(biotuning.peaks_ratios, 'Peaks_ratios_scale')
create_SCL(biotuning.extended_peaks_ratios_cons, 'consonant_extended_peaks_ratios_scale')

9. Plotting consonance matrix for a specific scale#

The metric_function can be set to:

  • None : the denominator of the normalized ratio

  • dyad_similarity : similarity with the harmonic series

  • consonance : (a+b)/(a*b)

from pytuning.visualizations.scales import consonance_matrix
scale = biotuning.euler_fokker_scale(method='extended_peaks', octave = 2)
cons_matrix = consonance_matrix(scale, vmin = 1, vmax = 100, cmap = 'magma_r', fig = None)
cons_matrix_harmsim = consonance_matrix(scale, metric_function = dyad_similarity,
                                        vmin = 0, vmax = 50, cmap = 'magma', fig = None)
../../_images/scale_construction_58_0.png ../../_images/scale_construction_58_1.png

10. Computing scale metrics#

The harmonicity of a scale can be assessed by different metrics.

From peaks ratios#

from biotuner.metrics import tuning_to_metrics
metrics_peaks = tuning_to_metrics(biotuning.peaks_ratios)
metrics_peaks
c:\Users\User\anaconda3\envs\biotuner\lib\site-packages\numpy\lib\function_base.py:380: RuntimeWarning: Mean of empty slice.
  avg = a.mean(axis)
c:\Users\User\anaconda3\envs\biotuner\lib\site-packages\numpy\core\_methods.py:189: RuntimeWarning: invalid value encountered in double_scalars
  ret = ret.dtype.type(ret / rcount)
{'sum_p_q': 211,
 'sum_distinct_intervals': 38,
 'metric_3': 21.5746031746032,
 'sum_p_q_for_all_intervals': 8497,
 'sum_q_for_all_intervals': 3508,
 'harm_sim': 27.5,
 'matrix_harm_sim': 9.289929639476492,
 'matrix_cons': 0.0972719610995822,
 'matrix_denom': 60.25}

From harmonic entropy tuning#

metrics_peaks = tuning_to_metrics(biotuning.HE_scale)
metrics_peaks
{'sum_p_q': 29847,
 'sum_distinct_intervals': 420,
 'metric_3': 151.999002802882,
 'sum_p_q_for_all_intervals': 901446,
 'sum_q_for_all_intervals': 416490,
 'harm_sim': 0.58,
 'matrix_harm_sim': 0.396151170895823,
 'matrix_cons': 0.0039701875712443635,
 'matrix_denom': 945.5333333333333}

11. Visualizing ratios with Lissajous curves#

A Lissajous curve is the graph of a system of parametric equations which describe the superposition of two perpendicular oscillations in x and y directions of different angular frequency (a and b). Hence, it allows to visualize ratio (a/b).

Thanks to https://glowingpython.blogspot.com/2011/12/lissajous-curves.html

from biotuner.vizs import lissajous_curves
tuning = biotuning.harmonic_fit_tuning
tuning = biotuning.peaks_ratios
lissajous_curves(tuning)
[8/7, 16/13, 23/16, 13/8, 23/14, 7/4, 23/13, 13/7, 2]
../../_images/scale_construction_65_1.png
lissajous_curves(gen_interval_tuning)
[1, 525/499, 1113/1000, 767/655, 965/779, 1259/966, 1103/800, 1233/850, 1177/767, 1558/965, 1177/655, 2]
../../_images/scale_construction_66_1.png

12. Listening to scales#

import biotuner
from biotuner.biotuner_utils import listen_scale
listen_scale(biotuning.peaks_ratios, 250, duration=0.5)
Scale: [1.1428571428571428, 1.2307692307692308, 1.4375, 1.625, 1.6428571428571428, 1.75, 1.7692307692307692, 1.8571428571428572, 2.0]
250
285.7142857142857
307.69230769230774
359.375
406.25
410.7142857142857
437.5
442.30769230769226
464.2857142857143
500.0
listen_scale(np.round(list(biotuning.HE_scale), 4), 100, duration=0.5)
Scale: [1.018 1.036 1.07  1.101 1.122 1.196 1.266 1.283 1.315 1.349 1.379 1.451
 1.494 1.57  1.603 1.646 1.71  1.792 1.818 1.927 1.967]
201.79999999999998
203.6
207.00000000000003
210.1
212.2
219.59999999999997
226.6
228.29999999999998
231.5
234.90000000000003
237.9
245.1
249.39999999999998
257.0
260.29999999999995
264.59999999999997
271.0
279.2
281.8
292.7
296.7