0

I have something like this : self.pushButton.clicked.connect(self.clicked)

I click my button and it calls the function clicked(), all good so far.

The Clicked() function plays a midi note and then does time.sleep() however instead of actually using time.sleep() i call another funciton loop() that does the time.sleep for me:

    def loop(self):
    print('we in the loop')
    global a
    for x in range(10):

        if a == 4:
            print('break')
            break

        else:
            print('0.1 second sleep')
            time.sleep(0.1)
                

This will do a 1 second time.sleep unless a == 4.clicked() will then turn off the midi note after the loop is finished.

Now i want it so if i click that same pushButton again while the loop is running then a = 4 and the loop will be broken and the original note will stop before 1 second has passed. e.g. if i click pushbutton while on the 5th cycle of the loop it will break, go back to clicked() and turn the note off early.

I want to be able to break this loop as soon as i have clicked the button for the second time.(the same button)

The problem is that while this loop is running it refuses to accept the second button press. Once the whole of Clicked() is finished running it will then que that second click, but i dont want this. I dont want to wait for Clicked to finish. I want to click the button while clicked() is running and have that second button press make a = 4 so that the loop will stop.

I have tried doing this with threading but it doesn't seem to make a difference, the second click of the button will not register until clicked() is finished.

What i want to see:

I click the button and it plays a midi note for 1 second. If i press that same button again before 1 second is up then it cuts the note early and starts the note again.

Basically i want to change the length of the loop if i click the button again.

Here is my actual code:

from PyQt5 import QtCore, QtGui, QtWidgets
import pygame
import pygame.midi
import time
import threading

pygame.midi.init()
player = pygame.midi.Output(0)
player.set_instrument(1)

a = 0

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(998, 759)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(430, 80, 121, 71))
        self.pushButton.setObjectName("pushButton")

        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 998, 21))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        'THIS IS THE BUTTON'
        self.pushButton.clicked.connect(self.clicked)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton.setText(_translate("MainWindow", "C"))

    'during each loop of loop() check to see if a == 4 and if it does then break'
    def loop(self):
        print('we in the loop')
        global a
        for x in range(10):

            if a == 4:
                print('break')
                break

            else:
                print('0.1 second sleep')
                time.sleep(0.1)



    def clicked(self):
        print('clicked')
        global player
        global a
        a = a + 2
        player.note_on(60, 127)
        self.loop()
        player.note_off(60, 127)
        










if __name__ == "__main__":
    import sys


    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()





    sys.exit(app.exec_())

I assume the answer has something to do with threading but when i tried this it made no difference because i had one thread in loop() and one thread in clicked() but this made no difference as the second click will never be recognised as long as clicked() is running because clicked() is the function the button is actually calling. I made clicked() do a = a + 2 so that on the second click i know that a == 4 and wanted to use that to end the loop.

Nimantha
  • 6,405
  • 6
  • 28
  • 69
Greg James
  • 15
  • 4
  • You must not call `sleep` in your UI code. Doing so will lock up the UI. You will have to use a timer. When you get a button click, you start your note, set up a timer, and return to the event loop. If the timer expires before you get another click, you stop the sound. In reality, programs like this set up a regular timer tick (say, 0.1 seconds) and have a schedule of "events" to be handled at a particular time. – Tim Roberts Apr 03 '21 at 20:52
  • ok, another way is to probably use threads and global variables – Matiiss Apr 03 '21 at 20:53
  • Remember that your UI cannot respond to events (like button clicks) unless you get back to your main loop. As long as you are in an event handler, your UI is unresponsive. You have to start thinking in terms of "event-driven programming". – Tim Roberts Apr 03 '21 at 20:56
  • You could use a QTimer and connect it's timeout signal to your clicked() method. – Heike Apr 03 '21 at 20:59
  • ah i see! this is exactly what i thought! It will not respond to a button click while still in the event handler. This is a problem for me, how do i go about solving this? – Greg James Apr 03 '21 at 21:04
  • @Matiiss It is not recommended to use global variables and less in threads since they are not thread-safe so many times you will get unexpected behaviors that will be difficult to track. – eyllanesc Apr 03 '21 at 21:41
  • @eyllanesc well thanks for the advice so how then should I 'communicate' between threads? – Matiiss Apr 03 '21 at 21:44
  • @Matiiss You have to use thread-safe means like queues, pipes, semaphores, etc. And in Qt you must use the signals. – eyllanesc Apr 03 '21 at 21:45
  • @Matiiss Your recommendation is like telling a pedestrian to cross the road blindfolded and there is a chance that they will have an accident or cross safely. Instead my recommendation is that you comply with the basic rules of the road such as using traffic lights or checking that there are no cars on the road. – eyllanesc Apr 03 '21 at 21:51
  • @eyllanesc well that is a bit confusing and perhaps true but as You may have noticed I am still pretty much learning (one of the ways is to teach others but that is irrelevant now). anyhow was that a comparison to something I did? if so what does the complyance with basic rules mean? bit confused on that one. not that You have to waste time and answer because I guess otherwise this could turn into a waste of time. so have a great day and thanks for the advice (why do I think this sounds passive aggressive when in fact that is not intended) nevermind this anyways – Matiiss Apr 03 '21 at 21:56
  • @Matiiss Traffic rules serve to minimize risks such as that to cross the road at a crossroads the traffic light must be in a certain color since it is thus known that it is unlikely (in theory) that a car will hit you. The same happens with resources (such as variables) since this way we avoid that in different contexts the same resource is used at the same time. – eyllanesc Apr 03 '21 at 22:01
  • @eyllanesc well thanks for the explanation. have a great day – Matiiss Apr 03 '21 at 22:02
  • @Matiiss See also https://en.wikipedia.org/wiki/Dining_philosophers_problem – eyllanesc Apr 03 '21 at 22:03

1 Answers1

2

Your code has several problems:

  • Do not use time.sleep in an eventloop even in small intervals since they will still block the eventloop and generate for example: GUI freeze, the slots associated with the signals are not invoked, etc.

  • Do not use global variables as they are difficult to debug.

In this case it is better to use a QTimer and implement the logic in a controller class, on the other hand do not modify the code generated by pyuic so in this case you will have to restore that code and save it in a ui.py file.

from functools import cached_property
from dataclasses import dataclass

from PyQt5 import QtCore, QtGui, QtWidgets
import pygame.midi

from ui import Ui_MainWindow


@dataclass
class MidiController(QtCore.QObject):
    note: int = 60
    velocity: int = 127
    channel: int = 0

    @cached_property
    def timer(self):
        timer = QtCore.QTimer(singleShot=True)
        timer.timeout.connect(self.off)
        return timer

    @cached_property
    def player(self):
        pygame.midi.init()
        player = pygame.midi.Output(0)
        player.set_instrument(1)
        return player

    def start(self, dt=1 * 1000):
        self.timer.setInterval(dt)
        self.timer.start()
        self.on()

    @property
    def running(self):
        return self.timer.isActive()

    def stop(self):
        self.timer.stop()
        self.off()

    def on(self):
        self.player.note_on(self.note, self.velocity, self.channel)

    def off(self):
        self.player.note_off(self.note, self.velocity, self.channel)


class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)

        self.pushButton.clicked.connect(self.handle_clicked)

    @cached_property
    def midi_controller(self):
        return MidiController()

    def handle_clicked(self):
        # update parameters
        self.midi_controller.note = 60
        # self.midi_controller.velocity = 127

        if self.midi_controller.running:
            self.midi_controller.stop()
        else:
            self.midi_controller.start()


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thankyou!! Going to take me a while to understand how to implement it but you have solved the question I asked, thankyou! – Greg James Apr 03 '21 at 22:31
  • If i wanted to give `handle_clicked()` an argument like `self.pushButton.clicked.connect(self.handle_clicked(60))` what changes would i have to make? for example if i wanted it to be `self.player.note_on(value1, 127)` and pass that argument all the way down from the original button click? I have tried this but run into an issue with `timer.timeout.connect(self.off)` as it doesnt want to take an argument im not sure why – Greg James Apr 04 '21 at 00:02
  • @GregJames I do not usually respond to new requests after responding since this would imply that it would never finish responding and that is not the objective of SO, so I will only update my answer for that particular case, but if you require another change then you will know what I will: I will take the trouble to answer you. See the update – eyllanesc Apr 04 '21 at 00:17
  • Thankyou!! You completely solved my problem! Your coding is very clean i need to learn to code like that. – Greg James Apr 04 '21 at 16:58