A python synthesizer sort of thing you can play on your computer or laptop keyboard or midi input.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

637 lines
17 KiB

#!/usr/bin/env python3
# Copyright (C) 2020 Harry Sentonbury
# GNU General Public License v3.0
from concurrent.futures import ThreadPoolExecutor
import sys
import threading
import mido
import numpy as np
import simpleaudio as sa
from PyQt5 import QtGui
from PyQt5.QtWidgets import (QApplication, QWidget, QSlider, QPushButton,
QListWidget, QLabel, QVBoxLayout)
from PyQt5.Qt import Qt
from PyQt5.QtGui import QIcon, QPixmap
class ChoosePort(QWidget):
def __init__(self):
super().__init__()
midi_input_names = mido.get_input_names()
self.setGeometry(100, 520, 600, 200)
self.setWindowTitle('Choose Midi Input')
self.setWindowIcon(QIcon('images/knotperfect-icon.png'))
self.setObjectName("midi_win")
layout = QVBoxLayout()
self.setLayout(layout)
self.listwidget = QListWidget()
for i, s in enumerate(midi_input_names):
self.listwidget.insertItem(i, s)
self.listwidget.clicked.connect(self.clicked)
layout.addWidget(self.listwidget)
self.info_label = QLabel(self)
self.info_label.setText("""Select port and wait a few seconds.
port will be open until program is closed""")
layout.addWidget(self.info_label)
self.close_kp_button = QPushButton("Close", self)
self.close_kp_button.move(400, 140)
self.close_kp_button.clicked.connect(lambda: self.close())
layout.addWidget(self.close_kp_button)
def clicked(self, qmodelindex):
midi_on_flag[0] = True
item = self.listwidget.currentItem().text()
do_it_int16()
activate_midi(item)
self.close()
class CustomKeybinder(QWidget):
def __init__(self):
super().__init__()
def change_key_list():
self.is_e_list = not self.is_e_list
if self.is_e_list:
self.selection_label.setText("key of E")
else:
self.selection_label.setText("key of C")
self.custom_e_list = []
self.is_e_list = True
self.setGeometry(400, 450, 600, 200)
self.setWindowTitle('Set Custom Keybindings')
self.setWindowIcon(QtGui.QIcon('images/knotperfect-icon.png'))
self.setObjectName("ck_win")
self.blah_label = QLabel(self)
self.blah_label.setText("Type the keys in the order you want, to whatever scale\n"
"then go back and click the Change Key button")
self.blah_label.move(10, 10)
self.list_label = QLabel(self)
self.list_label.setText("")
self.list_label.resize(550, 25)
self.list_label.move(10, 50)
self.list_label.setObjectName("l_label")
self.key_button = QPushButton("Select Key", self)
self.key_button.move(11, 100)
self.key_button.clicked.connect(change_key_list)
self.selection_label = QLabel(self)
self.selection_label.setText("key of E")
self.selection_label.move(130, 105)
self.close_ckb_button = QPushButton("Close", self)
self.close_ckb_button.move(420, 150)
self.close_ckb_button.clicked.connect(lambda: self.close())
def keyPressEvent(self, event):
if midi_on_flag[0] is True:
return
else:
if len(self.custom_e_list) >= 18:
if self.is_e_list:
e_keys[:] = self.custom_e_list[:]
else:
c_keys[:] = self.custom_e_list[:]
self.close()
self.custom_e_list.append(event.text())
self.list_label.setText(str(self.custom_e_list))
class MainWindow(QWidget):
def __init__(self):
super().__init__()
def keyPressEvent(self, event):
if midi_on_flag[0] is True:
return
else:
if mono_flag is True:
sa.stop_all()
try:
sound = key_notes.get(event.text())
play_obj = sa.play_buffer(sound, 2, 2, sample_rate)
except TypeError:
return
def do_it_int16():
def wave_maker(a):
factor = 2**(a / 12.0)
waveform_mod = (sine_wave(octave_freq * (
factor)) * sm) + (triangle(freq3 * factor, detune_freq) * tm)
waveform = (sine_wave(
octave_freq * factor) * sm) + (triangle(freq3 * factor) * tm)
waveform_detune = (sine_wave(
octave_freq * factor, detune_freq) * sm) + (
triangle(freq3 * factor, detune_freq) * tm)
waveform = ((waveform + waveform_detune) *
(waveform_mod / 2 + 0.5)) * 0.12 * volume
waveform[:np.size(attak)] *= attak
waveform[-np.size(fade):] *= fade
waveform = np.int16(waveform * 32767)
waveform2 = np.roll(waveform, roll_amount, axis=None)
waveform3 = np.vstack((waveform2, waveform)).T
waveform3 = waveform3.copy(order='C')
return waveform3
def sine_wave(f, detune=0.0):
y = np.sin((f + detune) * x + ramp_0 *
np.sin(((f + detune) * 0.5) * x + (np.sin(((f + detune) * fm) * x) * 0.5)))
return y
def triangle(f, detune=0.0):
y = 2 / np.pi * np.arcsin(np.sin((f + detune) * x + ramp_0 *
np.sin(((f + detune) * 0.5) * x + (np.sin(((f + detune) * fm) * x) * 0.5))))
return y * 0.8
applying_sliders_flag[0] = True
label_apply.hide()
fm = 0.25
freq3 = 440.0
detune_freq = detune_slider.value()
duration = duration_slider.value() / 10
octave_freq = (2**octave_slider.value()) * 220
ramp_amount = ramp_slider.value() / 10
roll_amount = delay_slider.value()
st = shape_slider.value() / 20
volume = volume_slider.value() / 100
tm = st * 1.2
sm = 1 - st
attack_size = attack_slider.value() / 100
fade_size = fade_slider.value() / 100
if midi_on_flag[0] is True:
num_range = midi_range
keys[:] = note_nums[:]
else:
if key_change_bool[0] is True:
num_range = c_range
else:
num_range = e_range
x = np.linspace(0, 2 * np.pi * duration, int(duration * sample_rate))
ramp_0 = np.logspace(1, 0, np.size(x), base=10) * ramp_amount
fade_size = int(np.size(x) * fade_size)
attak = np.linspace(0, 1, int(np.size(x) * attack_size))
fade = np.linspace(1, 0, fade_size if fade_size >= 1 else 1)
notes = []
with ThreadPoolExecutor() as executer:
notes = list(executer.map(wave_maker, num_range))
global key_notes
key_notes = dict(zip(keys, notes))
applying_sliders_flag[0] = False
return key_notes
def changed_duration():
val = duration_slider.value() * 0.1
duration_val_label.setText(str('{:2.1f}'.format(val)))
duration_val_label.adjustSize()
show_apply()
def changed_detune():
val = detune_slider.value()
detune_val_label.setText(str('{:2.1f}'.format(val)))
detune_val_label.adjustSize()
show_apply()
def changed_octave():
"""update octave label"""
val = (2**octave_slider.value()) * 220
octave_val_label.setText(str(val))
octave_val_label.adjustSize()
show_apply()
def changed_ramp():
val = ramp_slider.value() * 0.1
ramp_val_label.setText(str('{:2.1f}'.format(val)))
ramp_val_label.adjustSize()
show_apply()
def changed_roll():
val = delay_slider.value()
delay_val_label.setText(str('{:4.3f}\nSeconds'.format(val / 48000)))
delay_val_label.adjustSize()
show_apply()
def changed_attack():
val = attack_slider.value() * 0.01
attack_val_label.setText(str('{:3.2f}'.format(val)))
attack_val_label.adjustSize()
show_apply()
def changed_fade():
val = fade_slider.value() * 0.01
fade_val_label.setText(str('{:3.2f}'.format(val)))
fade_val_label.adjustSize()
show_apply()
def changed_vol():
val = volume_slider.value() * 0.01
vol_val_label.setText(str("{:3.2f}".format(val)))
vol_val_label.adjustSize()
show_apply()
def show_apply():
label_apply.show()
def flagger():
global mono_flag
mono_flag = not mono_flag
if mono_flag is True:
mono_button.setText('Mono')
else:
mono_button.setText('Poly')
def change_key():
if midi_on_flag[0] is True:
return
else:
key_change_bool[0] = not key_change_bool[0]
if key_change_bool[0] is True:
keys[:] = c_keys[:]
key_button.setText('C4')
else:
keys[:] = e_keys[:]
key_button.setText('E4')
do_it_int16()
def open_custom_kb_dialog():
if midi_on_flag[0] is True:
return
else:
custom_kb_dialog = CustomKeybinder()
custom_kb_dialog.show()
def open_midi_dialog():
if midi_on_flag[0] is True:
print("Close cutie-dead-synth to change midi")
else:
midi_dialog = ChoosePort()
midi_dialog.show()
def midi_stuff(port_arg):
def play_note(event):
global key_notes
if applying_sliders_flag[0] is True:
return
else:
if mono_flag is True:
sa.stop_all()
try:
sound = key_notes.get(event)
buffer_object = sa.play_buffer(sound, 2, 2, sample_rate)
except TypeError:
return
if len(port_arg) > 1:
portname = port_arg
else:
portname = None # Use default port
with mido.open_input(portname) as port:
print('Using {}'.format(port))
print('Waiting for messages...')
for message in port:
if message.type == 'note_on' and message.velocity > 0:
play_note(message.note)
def activate_midi(port_arg):
midi_thread = threading.Thread(target=midi_stuff, args=[port_arg], daemon=True)
midi_thread.start()
QSS = """
/* Styling */
QSlider::groove:horizontal {
border-radius: 1px;
height: 3px;
margin: 0px;
background-color: #222610;
}
QSlider::groove:horizontal:hover {
background-color: #21e4e4;
}
QSlider::handle:horizontal {
background-color: #728C00;
border: none;
height: 40px;
width: 20px;
margin: -10px 0;
border-radius: 10px;
padding: -20px 0px;
}
QSlider::groove:vertical {
border-radius: 1px;
width: 3px;
margin: 0px;
background-color: #222610;
}
QSlider::groove:vertical:hover {
background-color: #21e4e4;
}
QSlider::handle:vertical {
background-color: #728C00;
border: none;
width: 40px;
height: 20px;
margin: 0px -10px;
border-radius: 10px;
padding: 0px -20px;
}
QSlider:focus {
background-color: #0ba4a4;
}
MainWindow, QWidget#ck_win, QWidget#midi_win {
background-color: #313734;
}
QWidget#l_label {
background-color: #0ba4a4;
color: #313734;
}
QLabel {
color: #82edd8;
font-size: 14px;
}
QPushButton {
background-color: #728C00;
}
QPushButton:hover {
background-color: #e7ff00;
}
QPushButton:focus {
border: 2px solid #0ba4a4;
border-radius: 3px;
}
"""
c_keys = ['a', 'w', 's', 'e', 'd', 'f', 't',
'g', 'y', 'h', 'u', 'j', 'k', 'o', 'l', 'p', ';', '\'']
e_keys = ['a', 's', 'e', 'd', 'r', 'f', 't',
'g', 'h', 'u', 'j', 'i', 'k', 'l', 'p', ';', '[', '\'']
note_nums = [i for i in range(36, 97)]
midi_on_flag = [False]
keys = []
keys[:] = e_keys[:]
c_range = range(-9, 9)
e_range = range(-5, 13)
midi_range = range(-33, 28)
key_change_bool = [False]
applying_sliders_flag = [True]
mono_flag = False
sample_rate = 48000
app = QApplication(sys.argv)
app.setStyleSheet(QSS)
win = MainWindow()
win.setGeometry(20, 100, 700, 500)
win.setWindowIcon(QtGui.QIcon('images/knotperfect-icon.png'))
win.setWindowTitle('Not Perfect')
apply_button = QPushButton('Apply Settings', win)
apply_button.move(10, 10)
apply_button.clicked.connect(do_it_int16)
mono_button = QPushButton('Poly', win)
mono_button.move(200, 10)
mono_button.clicked.connect(flagger)
key_button = QPushButton('E4', win)
key_button.move(10, 290)
key_button.clicked.connect(change_key)
midi_button = QPushButton('Open Midi Input', win)
midi_button.move(10, 340)
midi_button.clicked.connect(open_midi_dialog)
# duration
duration_label = QLabel(win)
duration_label.setText('Duration')
duration_label.move(10, 50)
set0 = 14
duration_slider = QSlider(Qt.Horizontal, win)
duration_slider.setMinimumWidth(200)
duration_slider.setMinimum(0)
duration_slider.setMaximum(50)
duration_slider.setValue(set0)
duration_slider.setSingleStep(2)
duration_slider.move(80, 50)
duration_slider.valueChanged.connect(changed_duration)
duration_val_label = QLabel(win)
duration_val_label.setText(str('{:2.1f}'.format(set0 * 0.1)))
duration_val_label.move(290, 50)
# detune
detune_label = QLabel(win)
detune_label.setText('Detune')
detune_label.move(10, 90)
set1 = 9
detune_slider = QSlider(Qt.Horizontal, win)
detune_slider.setMinimumWidth(200)
detune_slider.setMinimum(0)
detune_slider.setMaximum(13)
detune_slider.setSingleStep(1)
detune_slider.setValue(set1)
detune_slider.move(80, 90)
detune_slider.valueChanged.connect(changed_detune)
detune_val_label = QLabel(win)
detune_val_label.setText(str(set1))
detune_val_label.move(290, 90)
# octave
octave_label = QLabel(win)
octave_label.setText('Octave')
octave_label.move(10, 130)
set2 = 1
octave_slider = QSlider(Qt.Horizontal, win)
octave_slider.setMinimumWidth(10)
octave_slider.setMinimum(0)
octave_slider.setMaximum(2)
octave_slider.setValue(set2)
octave_slider.move(80, 130)
octave_slider.valueChanged.connect(changed_octave)
octave_val_label = QLabel(win)
octave_val_label.setText(str((2**set2) * 220))
octave_val_label.move(190, 130)
# ramp_0
ramp_label = QLabel(win)
ramp_label.setText('Ramp')
ramp_label.move(10, 170)
set3 = 5
ramp_slider = QSlider(Qt.Horizontal, win)
ramp_slider.setMinimumWidth(200)
ramp_slider.setMinimum(0)
ramp_slider.setMaximum(20)
ramp_slider.setSingleStep(1)
ramp_slider.setValue(set3)
ramp_slider.move(80, 170)
ramp_slider.valueChanged.connect(changed_ramp)
ramp_val_label = QLabel(win)
ramp_val_label.setText(str(set3 * 0.1))
ramp_val_label.move(290, 170)
# roll
delay_label = QLabel(win)
delay_label.setText('Delay')
delay_label.move(10, 210)
set4 = 4000
delay_slider = QSlider(Qt.Horizontal, win)
delay_slider.setMinimumWidth(200)
delay_slider.setMinimum(0)
delay_slider.setMaximum(4000)
delay_slider.setSingleStep(100)
delay_slider.setValue(set4)
delay_slider.move(80, 210)
delay_slider.valueChanged.connect(changed_roll)
delay_val_label = QLabel(win)
delay_val_label.setText(str('{:4.3f}\nSeconds'.format(set4 / 48000)))
delay_val_label.move(290, 204)
# shape
sine_label = QLabel(win)
sine_label.setText('Sine')
sine_label.move(10, 250)
shape_slider = QSlider(Qt.Horizontal, win)
shape_slider.setMinimumWidth(200)
shape_slider.setMinimum(0)
shape_slider.setMaximum(20)
shape_slider.setSingleStep(1)
shape_slider.setValue(0)
shape_slider.move(80, 250)
shape_slider.valueChanged.connect(show_apply)
# attack
attack_label = QLabel(win)
attack_label.setText("Attack")
attack_label.move(368, 20)
attack_slider = QSlider(Qt.Vertical, win)
attack_slider.setMinimumHeight(100)
attack_slider.setMinimum(0)
attack_slider.setMaximum(100)
attack_slider.setSingleStep(1)
attack_slider.setValue(0)
attack_slider.move(380, 50)
attack_slider.valueChanged.connect(changed_attack)
attack_val_label = QLabel(win)
attack_val_label.setText(str('{:3.2f}'.format(0)))
attack_val_label.move(377, 160)
# fade
fade_label = QLabel(win)
fade_label.setText("Fade")
fade_label.move(442, 20)
set_7 = 20
fade_slider = QSlider(Qt.Vertical, win)
fade_slider.setMinimumHeight(100)
fade_slider.setMinimum(0)
fade_slider.setMaximum(100)
fade_slider.setSingleStep(1)
fade_slider.setValue(set_7)
fade_slider.move(450, 50)
fade_slider.valueChanged.connect(changed_fade)
fade_val_label = QLabel(win)
fade_val_label.setText(str('{:3.2f}'.format(set_7 * 0.01)))
fade_val_label.move(447, 160)
# volume
volume_label = QLabel(win)
volume_label.setText("volume")
volume_label.move(526, 20)
set_vol = 70
volume_slider = QSlider(Qt.Vertical, win)
volume_slider.setMinimumHeight(100)
volume_slider.setMinimum(0)
volume_slider.setMaximum(100)
volume_slider.setSingleStep(1)
volume_slider.setValue(set_vol)
volume_slider.move(540, 50)
volume_slider.valueChanged.connect(changed_vol)
vol_val_label = QLabel(win)
vol_val_label.setText(str("{:3.2f}".format(set_vol * 0.01)))
vol_val_label.move(538, 160)
triangle_label = QLabel(win)
triangle_label.setText('Triangle')
triangle_label.move(290, 250)
keybind_button = QPushButton("Custom Keyboard", win)
keybind_button.move(200, 290)
keybind_button.clicked.connect(open_custom_kb_dialog)
close_button = QPushButton('Close', win)
close_button.move(10, 390)
close_button.clicked.connect(lambda: app.quit())
label_pic = QLabel(win)
label_apply = QLabel(win)
# dce9e2
pixmap = QPixmap('images/knotpforQTsmall.png')
pixmap_1 = QPixmap('images/say_apply.png')
label_pic.setPixmap(pixmap)
label_pic.move(435, 180)
label_apply.setPixmap(pixmap_1)
label_apply.move(410, 181)
label_apply.hide()
do_it_int16()
win.show()
sys.exit(app.exec_())