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.
247 lines
8.4 KiB
247 lines
8.4 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
|
|
|
|
Laborejo2 is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import logging; logger = logging.getLogger(__name__); logger.info("import")
|
|
|
|
|
|
#Third Party
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
|
|
#Template Modules
|
|
from template.engine.pitch import baseNotesToAccidentalNames, plain, intervalAutomatic, diatonicIndex
|
|
|
|
#Our Modules
|
|
from .designer.customKeySignature import Ui_customKeySignature
|
|
import engine.api as api
|
|
|
|
|
|
class CustomKeySignatureWidget(QtWidgets.QDialog):
|
|
def __init__(self, mainWindow):
|
|
"""Init is called everytime the dialog gets shown"""
|
|
super().__init__(mainWindow)
|
|
self.mainWindow = mainWindow
|
|
self.ui = Ui_customKeySignature()
|
|
self.ui.setupUi(self)
|
|
|
|
|
|
self.ui.buttonBox.accepted.connect(self.process)
|
|
self.ui.buttonBox.rejected.connect(self.reject)
|
|
|
|
currentCursorPitch = api.getCursorPlainNote()
|
|
|
|
self.resultLabels = (
|
|
self.ui.label_result_c,
|
|
self.ui.label_result_d,
|
|
self.ui.label_result_e,
|
|
self.ui.label_result_f,
|
|
self.ui.label_result_g,
|
|
self.ui.label_result_a,
|
|
self.ui.label_result_b,
|
|
)
|
|
|
|
self.rootNotes = {
|
|
20 : self.ui.root_c ,
|
|
70 : self.ui.root_d ,
|
|
120 : self.ui.root_e ,
|
|
170 : self.ui.root_f ,
|
|
220 : self.ui.root_g ,
|
|
270 : self.ui.root_a ,
|
|
320 : self.ui.root_b ,
|
|
|
|
10 : self.ui.root_ces ,
|
|
60 : self.ui.root_des ,
|
|
110 : self.ui.root_ees ,
|
|
160 : self.ui.root_fes ,
|
|
210 : self.ui.root_ges ,
|
|
260 : self.ui.root_aes ,
|
|
310 : self.ui.root_bes ,
|
|
|
|
30 : self.ui.root_cis ,
|
|
80: self.ui.root_dis ,
|
|
130 : self.ui.root_eis ,
|
|
180: self.ui.root_fis ,
|
|
230 : self.ui.root_gis ,
|
|
280: self.ui.root_ais ,
|
|
330 : self.ui.root_bis ,
|
|
}
|
|
|
|
#Add reverse lookup
|
|
self.buttonToRootNote = {}
|
|
for key, value in self.rootNotes.items():
|
|
self.buttonToRootNote[value] = key
|
|
|
|
self.rootNotes[currentCursorPitch].setChecked(True)
|
|
|
|
for button in self.rootNotes.values():
|
|
button.clicked.connect(self.sanityCheck)
|
|
|
|
|
|
### Scale Radio Buttons.
|
|
### Please note that the names are just shorthands because we create a relative scale here.
|
|
### Those really should be roman numerals for first note, second note, third etc. but this is cumbersome to write
|
|
|
|
self.scaleButtons = (
|
|
(
|
|
self.ui.isis_c ,
|
|
self.ui.is_c ,
|
|
self.ui.nat_c ,
|
|
self.ui.es_c ,
|
|
self.ui.eses_c ,
|
|
),
|
|
(
|
|
self.ui.isis_d ,
|
|
self.ui.is_d ,
|
|
self.ui.nat_d ,
|
|
self.ui.es_d ,
|
|
self.ui.eses_d ,
|
|
),
|
|
(
|
|
self.ui.isis_e ,
|
|
self.ui.is_e ,
|
|
self.ui.nat_e ,
|
|
self.ui.es_e ,
|
|
self.ui.eses_e ,
|
|
),
|
|
(
|
|
self.ui.isis_f ,
|
|
self.ui.is_f ,
|
|
self.ui.nat_f ,
|
|
self.ui.es_f ,
|
|
self.ui.eses_f ,
|
|
),
|
|
(
|
|
self.ui.isis_g ,
|
|
self.ui.is_g ,
|
|
self.ui.nat_g ,
|
|
self.ui.es_g ,
|
|
self.ui.eses_g ,
|
|
),
|
|
(
|
|
self.ui.isis_a ,
|
|
self.ui.is_a ,
|
|
self.ui.nat_a ,
|
|
self.ui.es_a ,
|
|
self.ui.eses_a ,
|
|
),
|
|
(
|
|
self.ui.isis_b ,
|
|
self.ui.is_b ,
|
|
self.ui.nat_b ,
|
|
self.ui.es_b ,
|
|
self.ui.eses_b ,
|
|
),
|
|
)
|
|
|
|
for group in self.scaleButtons:
|
|
group[2].setChecked(True) #Make natural the default.
|
|
for button in group:
|
|
button.setStyleSheet("QRadioButton { font : 18px }")
|
|
button.clicked.connect(self.sanityCheck)
|
|
|
|
self.exec() #blocks until the dialog gets closed
|
|
|
|
|
|
def blockSignals(self, state:bool):
|
|
for group in self.scaleButtons:
|
|
for button in group:
|
|
button.blockSignals(state)
|
|
|
|
for button in self.rootNotes.values():
|
|
button.blockSignals(state)
|
|
|
|
|
|
|
|
def findRootPitch(self)->int:
|
|
for rootButton, basePitch in self.buttonToRootNote.items():
|
|
if rootButton.isChecked():
|
|
return basePitch
|
|
else:
|
|
raise RuntimeError()
|
|
|
|
|
|
def groupToInScalePitch(self, groupIndex:int, checkSpecificStep:int=None):
|
|
selectedRootPitch = self.findRootPitch()
|
|
stepInCMajor = (groupIndex+1)*50 - 30
|
|
|
|
#The transposed interval scale is also major, of course.
|
|
stepInMajorRootScale = intervalAutomatic(originalPitch=stepInCMajor, rootPitch=20, targetPitch=selectedRootPitch)
|
|
|
|
if checkSpecificStep is None:
|
|
#find the accidental mod
|
|
group = self.scaleButtons[groupIndex]
|
|
for counter, button in enumerate(reversed(group)): #group is isis to eses, so we need to reverse.
|
|
if button.isChecked():
|
|
#counter remains
|
|
break
|
|
else:
|
|
counter = checkSpecificStep
|
|
|
|
mod = 10*counter-20 #offset by -20 because the scale starts at -20 for double flat but we enumerate from 0
|
|
|
|
plainPitch = plain(stepInMajorRootScale + mod)
|
|
|
|
pitchOk = diatonicIndex(stepInMajorRootScale) == diatonicIndex(plainPitch) #did we cross a diatonic note boundary? That means this was a triple accidental
|
|
|
|
return plainPitch, pitchOk
|
|
|
|
|
|
|
|
def sanityCheck(self, buttonChecked:bool):
|
|
"""Called after every change to the whole widget.
|
|
Recalculates note names and checks if any of the steps result in triple accidentals.
|
|
|
|
We choose signal:clicked because signal:toggled triggers this signal twice because
|
|
for each radio toggle another radio button get's deactivated automatically."""
|
|
|
|
for groupIndex, group in enumerate(self.scaleButtons):
|
|
#Sanity check. Check for triple accidentals
|
|
for counter, button in enumerate(reversed(group)):
|
|
plainPitch, pitchOk = self.groupToInScalePitch(groupIndex, counter)
|
|
if pitchOk:
|
|
button.setEnabled(True)
|
|
else:
|
|
button.setEnabled(False)
|
|
if button.isChecked():
|
|
button.setChecked(False) #setChecked does NOT activate the clicked signal. We are safe from recursive calls.
|
|
group[2].setChecked(True) #Activate natural as fallback
|
|
|
|
#Calculate Labels
|
|
plainPitch, pitchOk = self.groupToInScalePitch(groupIndex)
|
|
assert pitchOk
|
|
self.resultLabels[groupIndex].setText(baseNotesToAccidentalNames[plainPitch])
|
|
|
|
|
|
def process(self):
|
|
selectedRootPitch = self.findRootPitch()
|
|
|
|
#Gather selected scale into engine scale list, which is the deviation from the major scale.
|
|
#THIS IS A RELATIVE SCALE, not accidentals for notes. The GUI shows roman numerals for scale building, not note names.
|
|
#-20 is double flat, -10 is flat, 0 is natural, 10 is sharp, 20 is double sharp
|
|
#self.scaleButtons is semi-correctly ordered: ascending scale (outer list) but from double sharp to double flat (inner list). we reverse the latter
|
|
deviationFromMajorScale = []
|
|
for group in self.scaleButtons:
|
|
for counter, button in enumerate(reversed(group)):
|
|
if button.isChecked():
|
|
deviationFromMajorScale.append(10*counter -20)
|
|
|
|
api.insertKeySignature(root=selectedRootPitch, scheme=deviationFromMajorScale)
|
|
|
|
self.done(True)
|
|
|