Issue
I'm trying to create an application which will help train a person's ear for hearing different pitches. I currently am working on an array of buttons which will have each chromatic note from octaves 3-5. When one of those buttons is pressed then it will execute the function above written with the pygame packaged.
The problem I am having is that once a button is pushed it seems that the entire application waits until that note has been played (just 1 second). The buttons will register inputs, and play them afterwards, just not at the same time. I want to be able to click any note at any time and have it be played.
import pygame
import time
import pygame.midi
# mixer config
# freq = 44100 # audio CD quality
# bitsize = -16 # unsigned 16 bit
# channels = 2 # 1 is mono, 2 is stereo
# buffer = 1024 # number of samples
# pygame.mixer.init(freq, bitsize, channels, buffer)
# pygame.mixer.music.set_volume(0.8)
pygame.midi.init()
fps = 60
timer = pygame.time.Clock()
player= pygame.midi.Output(0)
player.set_instrument(1,1) #127 is max
major=[0,4,7,12]
def go(note):
player.note_on(note, 127,1)
time.sleep(1)
player.note_off(note,127,1)
I am writing the application using pyqt5, relevant code for the buttons are below. I'm not sure if the issue is with how the function is being exectued with the buttons, or if it's with the pygame midi function.
import sys
import pygame
import time
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import chromatic_player
#small change for new branch
lowO= 3
highO = 5
Octaves = list(range(lowO,highO+1))
#Notes = ["A","A#/Bb","B","C","C#/Db","D","D#/Eb","E","F","F#/Gb","G"]
Notes = ["C","C#","D","D#","E","F","F#","G","A","A#","B"]
global glo
glo=0
class CustomButton(QPushButton):
def __init__(self, text='',octave='', parent=None):
self.octave = octave
self.text = text
super(QPushButton, self).__init__(text, parent=parent)
self.setGeometry(QRect(30, 40, 41, 41))
self.button_show()
self.setId = self.text
def button_show(self):
self.clicked.connect(self.on_click)
def on_click(self):
chromatic_player.go(int(Notes.index(self.text[:-1]))+12*self.octave)
print("User Clicked Me")
print(self.text)
print(glo + 1)
class Color(QWidget):
def __init__(self, color):
super(Color, self).__init__()
self.setAutoFillBackground(True)
palette = self.palette()
palette.setColor(QPalette.Window, QColor(color))
self.setPalette(palette)
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("My App")
layout1 = QHBoxLayout()
layout3 = QGridLayout()
layout1.setContentsMargins(0,0,0,0)
layout1.setSpacing(5)
layout1.addWidget(Color('green'))
layout3.addWidget(Color('red'))
layout3.addWidget(Color('purple'))
for j in Octaves:
layoutTemp = QVBoxLayout()
for i in Notes:
buttontemp = CustomButton("{}{}".format(i,j),j)
#buttontemp.clicked.connect(buttontemp.on_click)
layoutTemp.addWidget(buttontemp)
layout1.addLayout(layoutTemp)
layout1.addLayout( layout3 )
widget = QWidget()
widget.setLayout(layout1)
self.setCentralWidget(widget)
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
Solution
The truth is that you are making it wait, by calling time.sleep(1)
.
That completely blocks the event queue, which means that Qt can do absolutely nothing until that function returns, including reacting to user events or painting/updating widgets.
A proper way to do so is to call the note_on
and call the note_off
using a single shot QTimer.
A simple solution could be the following:
def go(note):
player.note_on(note, 127, 1)
QTimer.singleShot(1000, lambda:
player.note_off(note, 127, 1))
Note that a more appropriate implementation of your program should consider the following aspects:
- it should not be direct responsibility of the buttons to directly play the note, but to "request" it; a custom signal is certainly better for this;
- the player should probably be a class itself, so that you can eventually control its aspects more easily;
- it makes very little sense to get the note by reconstructing it from a string that was actually created from existing ranges and lists;
Also:
- you missed G sharp;
- your layout usage is confusing, and using multiple vertical layouts for widgets that would always be horizontally aligned makes very little sense: use a grid layout instead;
Considering the above, here is a possible class for the player:
class Player(object):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# note that the pygame initialization has been moved here
pygame.midi.init()
self.pygame_player = pygame.midi.Output(0)
self.pygame_player.set_instrument(1, 1)
def play(self, note):
self.pygame_player.note_on(note, 127, 1)
QTimer.singleShot(1000, lambda:
self.pygame_player.note_off(note, 127, 1))
The benefit of using a class is that it can be easily improved by adding features: for instance by using a "queue" of playing notes, so that you can eventually stop all of them before the timer ends; or change the instrument, default volume or note duration.
Here is an improved version of the button and the GUI:
class CustomButton(QPushButton):
playNote = pyqtSignal(int)
def __init__(self, note, octave, parent=None):
text = Notes[note] + str(octave)
super().__init__(text, parent=parent)
self.note = note + 12 * octave
self.clicked.connect(self.emitSignal)
def emitSignal(self):
self.playNote.emit(self.note)
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("My App")
self.player = Player()
mainLayout = QHBoxLayout()
mainLayout.setContentsMargins(0, 0, 0, 0)
mainLayout.addWidget(Color('green'))
buttonLayout = QGridLayout()
mainLayout.addLayout(buttonLayout)
rightLayout = QVBoxLayout()
mainLayout.addLayout(rightLayout)
rightLayout.addWidget(Color('red'))
rightLayout.addWidget(Color('purple'))
column = 0
for j in range(lowO, highO + 1):
for i in range(12):
buttontemp = CustomButton(i, j)
buttonLayout.addWidget(buttontemp, i, column)
buttontemp.playNote.connect(self.player.play)
column += 1
widget = QWidget()
widget.setLayout(mainLayout)
self.setCentralWidget(widget)
Note that the Color
widget will not properly show as custom widgets don't have a minimum size. You should add a basic setMinimumSize()
in its __init__
, or properly implement sizeHint()
.
Finally, don't use global
for the wrong reasons (which in OOP is, practically, always). Make it an instance attribute of some object: another benefit of using a class for the player is that you can use that glo
(which should really have a better name) for that instance, and increment it in there.
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.