Imports and utility classes
import warnings
import numpy as np
from scipy import signal
from ipywidgets import Button, IntSlider, Output, Layout, HBox, VBox, Dropdown
from ipywidgets import AppLayout, IntProgress, BoundedIntText, GridspecLayout
from IPython.display import Audio, display
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
import matplotlib.pyplot as plt
import sys
in_colab = 'google.colab' in sys.modules
from collections import namedtuple, OrderedDict
continuous_update = False
import asyncio
class Timer:
def __init__(self, timeout, callback):
self._timeout = timeout
self._callback = callback
self._task = asyncio.ensure_future(self._job())
async def _job(self):
await asyncio.sleep(self._timeout)
self._callback()
def cancel(self):
self._task.cancel()
def debounce(wait):
""" Decorator that will postpone a function's
execution until after `wait` seconds
have elapsed since the last time it was invoked. """
def decorator(fn):
if continuous_update:
timer = None
def debounced(*args, **kwargs):
nonlocal timer
def call_it():
fn(*args, **kwargs)
if timer is not None:
timer.cancel()
timer = Timer(wait, call_it)
return debounced
else:
return fn
return decorator
octave_cents = 1200
fifth_circle_octaves = 7
note_names = ['C', 'C#', 'D', 'D#', 'E',
'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
# Function to convert decimal to Roman Numerals
def roman(number):
num = [1, 4, 5, 9, 10, 40, 50, 90,
100, 400, 500, 900, 1000]
sym = ["I", "IV", "V", "IX", "X", "XL",
"L", "XC", "C", "CD", "D", "CM", "M"]
i = 12
rom = ''
while number:
div = number // num[i]
number %= num[i]
while div:
rom += sym[i]
div -= 1
i -= 1
return rom
# roman_nums = ['I', 'II', 'III', 'IV', 'V', 'VI',
# 'VII', 'VIII', 'IX', 'X', 'XI', 'XII']
num_halftones = len(note_names)
halftone_cents = round(octave_cents / num_halftones)
a4_hz = 440
a4_index = note_names.index('A')
def ratio2cents(ratio):
return np.log2(ratio) * octave_cents
def cents2ratio(cents):
return np.power(2, np.divide(cents, octave_cents))
def octave_fit(cents):
return cents % octave_cents
def tet_fit(cents):
return round(cents / halftone_cents) * halftone_cents
def stacked_fifths(num_notes, fifth_val, root):
note_cents = [octave_fit(n * fifth_val) for n in range(num_notes)]
ref_cents = note_cents[root]
note_cents = [round(octave_fit(cents - ref_cents)) for cents in note_cents]
note_cents.sort()
return note_cents
fifth_cents = ratio2cents(3 / 2) # Pure fifth
majthird_cents = ratio2cents(5 / 4) # Pure major third
note_freq_hz = [a4_hz * cents2ratio((n - a4_index) * halftone_cents) for n in range(num_halftones)]
syntcomm_cents = octave_fit(fifth_cents * 4) - majthird_cents # Syntonic comma
# Equal division of the octave
def edo(num_notes):
tone_step = octave_cents / num_notes
note_cents = [round(n * tone_step) for n in range(num_notes) ]
return note_cents
def pythagoras(num_notes=num_halftones):
return stacked_fifths(num_notes, fifth_cents, root=5)
def five_limit_just():
ratios = [1, 16/15, 9/8, 6/5, 5/4, 4/3,
45/32, 3/2, 8/5, 5/3, 9/5, 15/8]
note_cents = np.round(ratio2cents(ratios))
return note_cents
def seven_limit_just():
ratios = [1, 16/15, 9/8, 6/5, 5/4, 4/3,
7/5, 3/2, 8/5, 5/3, 7/4, 15/8]
note_cents = np.round(ratio2cents(ratios))
return note_cents
def meantone(num_notes=num_halftones, comma_split=4):
meantone_fifth = fifth_cents - syntcomm_cents / comma_split
return stacked_fifths(num_notes, meantone_fifth, root=5)
def twelve_tet():
note_cents = [round(n * halftone_cents) for n in range(num_halftones)]
return note_cents
def fifth_order(num_notes, first=0):
fifths = octave_fit(np.arange(num_notes) * fifth_cents)
ordered = np.argsort(np.argsort(fifths))
ordered = (np.append(ordered, 0) + first) % num_notes
return ordered
def get_note_name(cents, ref):
c_cents = octave_fit(cents + ref)
c_index = np.round(c_cents / halftone_cents).astype(np.int)
return note_names[c_index % num_halftones]
def get_cents(freq):
return ratio2cents(freq / note_freq_hz[0])
def get_error(cents, round_digits=None):
error = (cents2ratio(cents) - 1) * 100
if round_digits is not None:
e_sgn = np.sign(error)
round_power = 10 ** round_digits
e_ceil = np.ceil(np.abs(error) * round_power) / round_power
error = e_sgn * e_ceil
return error
pyth_name = "Pythagorean (pure fifths)"
edo_name = "Equally divided octave"
mtone_name = "Meantone (1/4-comma)"
Temperament = namedtuple('Temperament', ('name', 'get'))
Scale = namedtuple('Scale', ('name', 'notes', 'fifth_origin'))
Note = namedtuple('Note', ('name', 'pitch'))
root_notes = tuple(Note(nn, nf) for nn, nf in zip(note_names, note_freq_hz))
temp_chromatic = (Temperament("Equal temperament", twelve_tet),
Temperament("Just intonation (5-limit)", five_limit_just),
Temperament(pyth_name, pythagoras),
Temperament(mtone_name, meantone),
Temperament(edo_name, edo))
temp_microtonal = (Temperament(edo_name, edo),
Temperament(pyth_name, pythagoras),
Temperament(mtone_name, meantone))
scales = OrderedDict()
scales[1] = (Scale("Octave", (0,), 0),)
scales[2] = (Scale("Fifth", (0, 7), 0),
Scale("Fourth", (0, 5), 0),
Scale("Major third", (0, 4), 0),
Scale("Minor third", (0, 3), 0),
Scale("Major sixth", (0, 9), 0),
Scale("Minor sixth", (0, 8), 0),
Scale("Major seventh", (0, 11), 0),
Scale("Minor seventh", (0, 10), 0),
Scale("Major second", (0, 2), 0),
Scale("Minor second", (0, 1), 0),
Scale("Tritone", (0, 6), 0))
scales[3] = (Scale("Structural", (0, 5, 7), 0),
Scale("Major triad", (0, 4, 7), 0),
Scale("Minor triad", (0, 3, 7), 0),
Scale("Diminished triad", (0, 3, 6), 0),
Scale("Augmented triad", (0, 4, 8), 0))
scales[4] = (Scale("Major tetratonic", (0, 2, 5, 7), 2),
Scale("Minor tetratonic", (0, 3, 5, 10), 1),
Scale("Major seventh", (0, 4, 7, 11), 0),
Scale("Minor seventh", (0, 3, 7, 10), 0),
Scale("Dominant seventh", (0, 4, 7, 10), 0),
Scale("Diminished seventh", (0, 3, 6, 9), 0),
Scale("Half-diminished seventh", (0, 3, 6, 10), 0),
Scale("Minor-major seventh", (0, 3, 7, 11), 0))
scales[5] = (Scale("Major pentatonic", (0, 2, 4, 7, 9), 0),
Scale("Minor pentatonic", (0, 3, 5, 7, 10), 0))
scales[6] = (Scale("Major hexatonic", (0, 2, 4, 5, 7, 9), 3),
Scale("Minor hexatonic", (0, 2, 3, 5, 7, 10), 2),
Scale("Whole-tone", (0, 2, 4, 6, 8, 10), 0),
Scale("Blues scale", (0, 3, 5, 6, 7, 10), 4))
scales[7] = (Scale("Major scale", (0, 2, 4, 5, 7, 9, 11), 0),
Scale("Minor scale", (0, 2, 3, 5, 7, 8, 10), 0),
Scale("Harmonic minor", (0, 2, 3, 5, 7, 8, 11), 0),
Scale("Lydian mode", (0, 2, 4, 6, 7, 9, 11), 0),
Scale("Mixolyidian mode", (0, 2, 4, 5, 7, 9, 10), 0),
Scale("Dorian mode", (0, 2, 3, 5, 7, 9, 10), 0),
Scale("Phryigian mode", (0, 1, 3, 5, 7, 8, 10), 0),
Scale("Locrian mode", (0, 1, 3, 5, 6, 8, 10), 0))
scales[12] = (Scale("Chromatic", tuple(range(num_halftones)), 0),)
scales[17] = (Scale("-", 17, 0),)
scales[24] = (Scale("Quarter-tone", 24, 0),)
scales[31] = (Scale("-", 31, 0),)
scales[53] = (Scale("-", 53, 0),)
scale_cols= {1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1,
7: 1, 12: 1, 17: 3,
24: 4, 31: 4, 53: 6}
valid_num_notes = tuple(scales.keys())
default_num_notes = 7
def get_notes(temperament, notes, root=0):
if type(notes) is int:
tempered_notes = temperament.get(notes)
f_ref = note_freq_hz[root]
note_tuple = tuple(Note('', tempered_notes[n]) for n in range(notes))
elif type(notes) is tuple:
tempered_notes = temperament.get()
shifted_notes = [(n + root) % num_halftones for n in notes]
f_ref = note_freq_hz[root]
cent_ref = tempered_notes[root]
note_tuple = tuple(Note(note_names[n],
octave_fit(tempered_notes[n] - cent_ref)) for n in shifted_notes)
else:
raise ValueError("notes must be an integer or a tuple of integers.")
return note_tuple, f_ref
My very own minimalist synth implementation in less than 200 lines of code!!
sampling_rate = 44100 # Sample rate (Hz)
def time2samples(t, rate):
"""
Convert time to number of samples at a certain rate.
:param t: Time in seconds
:param rate: Sampling rate.
:return: Number of samples for the given time at the given rate.
"""
return round(t * rate)
def db2mag(db):
return 10 ** (db / 20)
def piecewiselin(x, y):
"""
Create piecewise linear array
:param x: Lengths of the segments in number of samples.
:param y: Point values. Its length must be 1 greater than the lenght of x.
:return: Piecewise linear array.
"""
xlen = len(x)
ylen = len(y)
if (ylen - xlen) != 1:
raise ValueError(f"Invalid dimensions len(x)={xlen}, len(y)={ylen}. len(x) must equal len(y) - 1.")
y_start = y[:-1]
y_stop = y[1:]
pwl = np.array([])
for start, stop, num in zip(y_start, y_stop, x):
pwl = np.append(pwl, np.linspace(start, stop, num))
return pwl
def adsr(adsr_durations, adsr_levels, total_duration=None, sustain_fade=1, smooth_factor=1):
n_attack, n_decay, n_sustain, n_release = adsr_durations
keystroke_duration = n_attack + n_decay + n_sustain + n_release
db_floor, db_peak, db_sustain, db_release = adsr_levels
if total_duration is not None:
if total_duration < keystroke_duration:
raise ValueError("Total duration must be greater or equal than the overall keystroke duration")
fadeout = total_duration - keystroke_duration
db_slope = (db_floor - db_sustain) / n_release
db_release += db_slope * fadeout
n_release += fadeout
if max(adsr_levels) > -6.0:
raise warnings.warn(f"Your dB levels are {adsr_levels}. Levels greater than -6 dB may saturate.", UserWarning)
#################################################################
# #
# ADSR Curve #
# dB Level ^ #
# | #
# Peak | X #
# | X XX #
# | X XX #
# Sustain | X XXXXXXXXXXXXX #
# | X XX #
# | X XX #
# | X XX #
# | X XX #
# Floor |X X #
# +--------------------------------------------> Time #
# |-----|------------|---------|-------|-------| #
# Attack Decay Sustain Release Fadeout #
#################################################################
if smooth_factor > 1:
n_attack //= smooth_factor
n_decay //= smooth_factor
n_sustain //= smooth_factor
n_release //= smooth_factor
adsr_curve = piecewiselin((n_attack, n_decay, n_sustain, n_release),
(db_floor, db_peak, db_sustain,
db_sustain - sustain_fade, db_release))
if smooth_factor > 1:
adsr_curve = signal.resample(adsr_curve, total_duration, window="hamming")
magnitudes = db2mag(adsr_curve)
return magnitudes
class Filter:
"""
Apply Butterworth lowpass-filter to signal
:param x: Input signal.
:param fc: Cutoff frequency for the filtering. Default is 8 kHz.
:param fs: Sampling frequency. Default is 44.1 kHz
:param order: Order of the butterworth lowpass filter. Default is 4
:return: Filtered signal
"""
def __init__(self, fc, order, fs):
b, a = signal.butter(order, fc * 2 / fs)
self.b = b
self.a = a
self._initial = signal.lfilter_zi(b, a)
self.z = self._initial
def lfilter(self, x):
y, zf = signal.lfilter(self.b, self.a, x, zi=self.z)
self.z = zf
return y
def reset(self):
self.z *= 0
def sawtooth_harmonics(f0, t, n_harmonics=10, fundamental=1.0,
power=1, random_phase=True):
wave = np.zeros_like(t)
for n in np.arange(1, n_harmonics + 1):
if n == 1:
a = fundamental
else:
a = 1 / (n ** power)
phase = 2 * np.pi * f0 * n * t
if random_phase:
phase += 2 * np.pi * np.random.rand()
wave += a * np.sin(phase)
return wave
lpf = Filter(fc=4000, order=6, fs=sampling_rate)
def synthesize(note_frequencies, fref=261.626, fs=sampling_rate, fadeout=1,
attack=.1, decay=.1, sustain=-15, release=.7, sus_time=.4,
floor=-90, peak=-12, rel_level=-90, spacing=.6, chord_notes=4,
progress=None):
n_freqs = len(note_frequencies)
octave_chord = n_freqs == 2
chord = n_freqs <= chord_notes + 1
attack_samples = time2samples(attack, fs)
decay_samples = time2samples(decay, fs)
sustain_samples = time2samples(sus_time, fs)
release_samples = time2samples(release, fs)
fadeout_samples = time2samples(fadeout, fs)
sample_spacing = time2samples(spacing, fs)
chord_length = sample_spacing + fadeout_samples * 2 if chord else 0
total_samples = sample_spacing * n_freqs + fadeout_samples + chord_length
adsr_samples = (attack_samples, decay_samples,
sustain_samples, release_samples)
adsr_levels = (floor, peak, sustain, rel_level)
chord_adsr_ivals = (attack_samples, decay_samples,
2 * sustain_samples, release_samples)
adsr_curve = adsr(adsr_samples, adsr_levels)
adsr_chord = adsr(chord_adsr_ivals, adsr_levels, total_duration=chord_length) if chord else 0
data = np.zeros(total_samples)
t_length = adsr_curve.size
max_length = np.max((chord_length, t_length))
t = np.arange(max_length) / fs
lpf.reset()
for n, f_cent in enumerate(note_frequencies):
last_note = n == n_freqs - 1
t_offset = n * sample_spacing
t_wave = t_offset + t_length
t_filter = total_samples if last_note else (n + 1) * sample_spacing
f_hz = fref * np.power(2, f_cent / octave_cents)
waveform = sawtooth_harmonics(f_hz, t, power=1.7)
wave = waveform[:t_length] * adsr_curve
data[t_offset:t_wave] += wave
if chord and ((not last_note) or octave_chord):
wave = waveform[:chord_length] * adsr_chord
data[-chord_length:] += wave
data[t_offset:t_filter] = lpf.lfilter(data[t_offset:t_filter])
if progress is not None:
progress(n + 1)
return data
def compress(x):
peak = np.abs(x).max()
if peak > 1:
return x / peak
else:
return x
def parse_options(options):
return [(o.name, n) for n, o in enumerate(options)]
def react(*args):
for a in args:
a.react = True
def unreact(*args):
for a in args:
a.react = False
audio_height = "auto"
btn_width = 55
head_layout = {'height': 'auto', 'width': 'auto'}
btn_layout = {'height': 'auto', 'width': f'{btn_width}%'}
output_layout = {'align_items': 'center', 'align_content': 'center'}
style = {'description_width': f'{100 - btn_width}%'}
tuner_box = VBox([], layout=Layout(align_content='stretch', align_items='stretch',
justify_content='flex-start', height="100%",
width="100%", overflow="hidden"))
cent_margin = 5
def clip_neighbours(cell):
num_notes = btn_notes.value
note_widgets = cell.pool[:num_notes]
i_note = cell.index
low_note = note_widgets[(i_note - 1) % num_notes]
high_note = note_widgets[(i_note + 1) % num_notes]
if low_note is not note_widgets[-1]:
low_note.max = cell.value - cent_margin
if high_note is not note_widgets[0]:
high_note.min = cell.value + cent_margin
def clip_value(change):
owner = change["owner"]
if owner.react:
pool = owner.pool
if pool is sliders_pool:
num_notes = btn_notes.value
note_widgets = owner.pool[:num_notes]
i_note = owner.index
low_note = note_widgets[(i_note - 1) % num_notes]
high_note = note_widgets[(i_note + 1) % num_notes]
low_value = low_note.value + cent_margin
high_value = high_note.value - cent_margin
if owner is note_widgets[0]:
low_value -= octave_cents
elif owner is note_widgets[-1]:
high_value += octave_cents
unreact(owner)
if owner.value < low_value:
owner.value = low_value
elif owner.value > high_value:
owner.value = high_value
change["new"] = owner.value
update_name(change)
plot_notes(change)
react(owner)
elif pool is cell_pool:
clip_neighbours(owner)
else:
raise RuntimeError("Unrecognized pool")
def notes2theta(note_list):
theta = np.array(note_list) * 2 * np.pi / octave_cents
return theta
plot_out = Output(layout=Layout(align_content='center', align_items='center',
justify_content='center', min_height="0px",
justify_items='center'))
def init_plot():
with plot_out:
plt.close(2)
fig = plt.figure(2)
fig.canvas.header_visible = False
axis = fig.add_subplot(1, 1, 1, frameon=False, projection='polar')
axis.set_theta_direction(-1)
axis.set_theta_zero_location('N')
axis.spines['polar'].set_visible(False)
axis.set_yticks([1])
axis.set_yticklabels([])
axis.set_ylim(0, 1.1)
plt.grid(False, axis='x')
pline, = axis.plot(1, 1, 'tab:purple', marker="o", mfc="k", mec="k")
pline.r = [1]
plt.tight_layout(pad=0.0)
return pline, axis
def set_notes(notes):
pline, axis = init_plot()
theta = notes2theta(notes)
pline.set_data(theta[btn_scale.fifth_order], btn_notes.r)
axis.set_xticks(theta)
axis.set_xticklabels(btn_scale.xticklabels)
audio_layout = Layout(width="100%", align_items='flex-end')
audio_bar = IntProgress(description="Generating notes",
min=0, max=default_num_notes,
value=0, style=style, layout=Layout(**head_layout))
audio_out = Output(layout=audio_layout)
audio_out.f_root = a4_hz
audio_out.fs = sampling_rate
audio_out.player = None
def plot_notes(change):
if not hasattr(change, 'owner') or change["owner"].react:
with plot_out:
if type(change) is tuple:
notes = [nt.pitch for nt in change]
else:
notes = [ns.value for ns in change["owner"].pool[:btn_notes.value]]
set_notes(notes)
plot_out.clear_output(wait=True)
plt.tight_layout(pad=0.0)
plt.show()
@debounce(.2)
def generate_notes(change):
if not hasattr(change, 'owner') or change["owner"].react:
loading_audio()
def audio_progress(p): audio_bar.value = p
if type(change) is tuple:
freqs = [nt.pitch for nt in change]
else:
freqs = [ns.value for ns in change["owner"].pool[:btn_notes.value]]
freqs.append(freqs[0] + octave_cents)
audio_data = synthesize(freqs, fref=audio_out.f_root,
fs=audio_out.fs, progress=audio_progress)
audio_data = compress(audio_data)
audio_kwargs = {"rate": audio_out.fs}
if not in_colab:
audio_kwargs["normalize"] = False
if audio_out.player is None:
audio = Audio(audio_data, autoplay=False, **audio_kwargs)
audio_out.player = audio
else:
audio_bytes = audio_out.player._make_wav(audio_data, **audio_kwargs)
audio_out.player.data = audio_bytes
audio_out.data = audio_data
with audio_out:
audio_out.clear_output(wait=True)
display(audio_out.player)
audio_ready()
def tune(change=None):
if not hasattr(change, 'owner') or change["owner"].react:
loading_audio()
n_notes = btn_notes.value
root = btn_root.value
temperament = btn_temperament.temperament[btn_temperament.value]
scale = scales[n_notes][btn_scale.value]
if temperament.name == edo_name or n_notes > num_halftones:
scale_notes = n_notes
else:
scale_notes = scale.notes
default_notes, f_root = get_notes(temperament, scale_notes, root)
audio_out.f_root = f_root
btn_scale.cents_root = get_cents(f_root)
btn_scale.fifth_order = fifth_order(n_notes, first=scale.fifth_origin)
if n_notes > num_halftones:
btn_scale.xticklabels = []
else:
btn_scale.xticklabels = [dn.name for dn in default_notes]
cols = scale_cols[n_notes]
if cols <= 1:
pool = sliders_pool
unreact(*pool)
for tune_widget, nt in zip(pool, default_notes):
tune_widget.description = nt.name
tune_widget.value = nt.pitch
react(tune_widget)
show_tuner(n_notes)
else:
pool = cell_pool
unreact(*pool)
for tune_widget, nt in zip(pool, default_notes):
tune_widget.description = nt.name
tune_widget.min = 0
tune_widget.max = octave_cents
tune_widget.value = nt.pitch
for tune_widget in pool[:n_notes]:
clip_neighbours(tune_widget)
react(tune_widget)
show_tuner(n_notes, cols=cols)
plot_notes(default_notes)
generate_notes(default_notes)
def update_name(change):
n_notes = btn_notes.value
widget = change["owner"]
if widget.react and not btn_temperament.edo and n_notes <= num_halftones:
cents = change["new"]
name = get_note_name(cents, btn_scale.cents_root)
if name != widget.description:
widget.description = name
btn_scale.xticklabels[widget.index] = name
sliders_pool = ()
for n in range(num_halftones):
new_slider = IntSlider(min=0, max=octave_cents, step=1, disabled=False,
continuous_update=continuous_update,
layout=Layout(width='auto', height="auto",
min_height="0px"), style={'description_width': "20px"})
new_slider.index = n
new_slider.observe(clip_value, 'value')
new_slider.observe(update_name, 'value')
new_slider.observe(plot_notes, 'value')
new_slider.observe(generate_notes, 'value')
sliders_pool += new_slider,
for slider in sliders_pool:
slider.pool = sliders_pool
unreact(*sliders_pool)
cell_pool = ()
for n in range(max(valid_num_notes)):
new_cell = BoundedIntText(min=0, max=octave_cents, step=1, disabled=False,
continuous_update=continuous_update,
layout=Layout(width='auto', min_width="5%"),
style={'description_width': "15%"})
new_cell.index = n
new_cell.observe(clip_value, 'value')
new_cell.observe(update_name, 'value')
new_cell.observe(plot_notes, 'value')
new_cell.observe(generate_notes, 'value')
cell_pool += new_cell,
unreact(*cell_pool)
for c in cell_pool:
c.pool = cell_pool
def select_n_notes(change):
n_notes = change["new"]
audio_bar.max = n_notes
btn_notes.r = np.ones(n_notes + 1)
loading_audio()
unreact(btn_temperament, btn_root, btn_scale)
if n_notes > num_halftones:
if btn_temperament.temperament is not temp_microtonal:
btn_temperament.temperament = temp_microtonal
btn_temperament.value = 0
btn_temperament.options = parse_options(temp_microtonal)
btn_scale.disabled = True
else:
if btn_temperament.temperament is not temp_chromatic:
btn_temperament.temperament = temp_chromatic
btn_temperament.value = 0
btn_temperament.options = parse_options(temp_chromatic)
btn_scale.disabled = False
hide_tuner()
btn_scale.options = parse_options(scales[n_notes])
btn_scale.value = 0
tune()
react(btn_temperament, btn_root, btn_scale)
def select_temperament(change):
new_temp = btn_temperament.temperament[change["new"]]
edo_selected = new_temp.name == edo_name
btn_scale.disabled = edo_selected
btn_temperament.edo = edo_selected
tune(change)
btn_notes = Dropdown(options=valid_num_notes, value=default_num_notes,
description="Number of Notes", disabled=False,
style=style,
layout=Layout(**head_layout))
btn_root = Dropdown(options=parse_options(root_notes), value=0,
description="Tonic/Root", disabled=False,
style=style,
layout=Layout(**head_layout))
btn_temperament = Dropdown(options=parse_options(temp_chromatic),
value=0, description="Tuning",
style=style,
disabled=False, layout=Layout(**head_layout))
btn_temperament.edo = False
btn_temperament.temperament = temp_chromatic
btn_scale = Dropdown(options=parse_options(scales[default_num_notes]),
value=0, description="Scale/Chord",
style=style,
disabled=False, layout=Layout(**head_layout))
btn_reset = Button(description="Reset", layout=Layout(**btn_layout))
react(btn_temperament, btn_root, btn_scale)
btn_notes.observe(select_n_notes, 'value')
btn_temperament.observe(select_temperament, 'value')
btn_root.observe(tune, 'value')
btn_scale.observe(tune, 'value')
btn_reset.on_click(tune)
layout_right = Layout(width="auto",
display='flex',
flex_flow='column',
align_items='flex-end')
right_box = HBox(children=[btn_reset],
layout=layout_right)
buttons = (btn_notes, btn_root, btn_temperament,btn_scale, right_box)
ui = AppLayout(pane_widths=[4, 0, 5], pane_heights=[0, 6, 4],
grid_gap="0px",
layout=Layout(justify_items="center", height="290px",
align_content='flex-start', align_items='flex-start'))
ui.right_sidebar = tuner_box
ui.left_sidebar = VBox(buttons + (audio_out,),
layout=Layout(width="100%", justify_content='flex-start'))
ui.loading = False
ui.visible_tuner = True
def loading_audio():
if not ui.loading:
audio_bar.value = 0
ui.left_sidebar.children = buttons + (audio_bar,)
ui.loading = True
def audio_ready():
if ui.loading:
ui.left_sidebar.children = buttons + (audio_out,)
ui.loading = False
def hide_tuner():
if ui.visible_tuner:
tuner_box.children = ()
ui.visible_tuner = False
def show_tuner(n_notes, cols=None):
if cols is None:
tuner_box.children = sliders_pool[:n_notes]
ui.right_sidebar= tuner_box
else:
rows = int(np.ceil(n_notes / cols))
grid = GridspecLayout(rows, cols, layout=Layout(align_content='stretch', align_items='stretch',
justify_content='flex-start', height="auto"))
for n in range(n_notes):
grid[n // cols, n % cols] = cell_pool[n]
ui.right_sidebar= grid
ui.visible_tuner = True
plot_box = HBox([plot_out], layout=Layout(align_content='center', align_items='center',
justify_content='center', justify_items='center',
padding="0.0"))
select_n_notes({"old": 0, "new": default_num_notes})
display(plot_box, ui)