2

I am developing a GUI which can record audio for an arbitrary duration using sounddevice and soundfile libraries. The recording process is stopped by pressing 'ctrl+c' button combination.

I am trying to implement a GUI with a 'start' and 'end' button. The 'start' button should invoke the record function and the 'end' button should simulate 'ctrl+c' event. I don't know how this event can be implemented as a function in python. An idea to implement is much appreciated.

The code runs properly if you run it using windows command prompt python record.py

record.py is as follows:

from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import time
import queue
from PyQt5 import QtCore, QtGui, QtWidgets
import soundfile as sf
import sounddevice as sd
import mythreading


class Ui_MainWindow(object):
    def __init__(self):
        self.threadpool = QThreadPool()
        print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())

    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(640, 480)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(280, 190, 75, 23))
        self.pushButton.setObjectName("pushButton")
        self.pushButton.clicked.connect(self.start_button_func)

        self.pushButton_1 = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton_1.setGeometry(QtCore.QRect(380, 190, 75, 23))
        self.pushButton_1.setObjectName("pushButton")
        self.pushButton_1.clicked.connect(self.end_button_func)

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

        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", "Start"))
        self.pushButton_1.setText(_translate("MainWindow", "End"))

    def record(self):
        self.q = queue.Queue()
        self.s = sd.InputStream(samplerate=48000, channels=2, callback=self.callback)
        try:
            # Make sure the file is open before recording begins
            with sf.SoundFile('check.wav', mode='x', samplerate=48000, channels=2, subtype="PCM_16") as file:
                with self.s:
                    # 1 second silence before the recording begins
                    time.sleep(1)
                    print('START')
                    print('#' * 80)
                    print('press Ctrl+C to stop the recording')
                    while True:
                        file.write(self.q.get())
        except OSError:
            print('The file to be recorded already exists.')
            sys.exit(1)
        except KeyboardInterrupt:
            print('The utterance is recorded.')

    def callback(self, indata, frames, time, status):
        """
        This function is called for each audio block from the record function.
        """

        if status:
            print(status, file=sys.stderr)
        self.q.put(indata.copy())

    def start_button_func(self):
        self.worker = mythreading.Worker(self.record)
        self.threadpool.start(self.worker)

    def end_button_func(self):
        print('how to stop?')


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_())

mythreading.py is as follows:

from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *


class Worker(QRunnable):

    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()
        self.fn = fn

    @pyqtSlot()
    def run(self):
        self.fn()
M.K
  • 1,464
  • 2
  • 24
  • 46
  • 1
    `raise KeyboardInterrupt` – SuperShoot Nov 27 '18 at 10:44
  • @SuperShoot Could you please explain a bit more? I want to generate the 'ctrl+c' using a button press. Not gonna use keyboard at all. –  Nov 27 '18 at 10:49
  • When you manually press `ctrl+c`, a `KeyboardInterrupt` is raised in the main thread of the running application. You can programmatically raise that exception like any other with the `raise` keyword. – SuperShoot Nov 27 '18 at 10:54
  • Related: https://stackoverflow.com/questions/35571440/when-is-keyboardinterrupt-raised-in-python – SuperShoot Nov 27 '18 at 10:55
  • 1
    So you want to recreate what `ctrl+c` does (i.e. send a `SIGINT`) to the recording process. Does this answer do that: https://stackoverflow.com/questions/27356837/send-sigint-in-python-to-os-system ? I'd suggest running the recording process from Python in a subprocess, then you can kill as needed. – Ed Smith Nov 27 '18 at 10:56
  • I am sorry, I can't understand what you are trying to explain. The record function is already looking for a keyboard interrupt ('ctrl+c') to stop the recording. I want to generate this with a button click on the GUI. It would be great if you can edit the code accordingly. –  Nov 27 '18 at 10:57
  • @EdSmith Unfortunately, running it as subprocess doesn't solve the problem. –  Nov 27 '18 at 11:08
  • 1
    @M.Denis, as in you can't use subprocesses (because the record is started elsewhere) or trying to terminate the record process, which was started as a subprocess in Python (using `os.kill(pid, signal.SIGINT)` as in the linked answer) does not work? – Ed Smith Nov 27 '18 at 11:16
  • @EdSmith Unfortunately, No! signal.SIGINT is for Linux and I tried using signal.CTRL_C_EVENT for windows. –  Nov 27 '18 at 11:20
  • 1
    @M.Denis, I see you are using the packages `soundfile` and `sounddevice`, worth mentioning at the top of your question as I think it may be these that need to be terminated. Could you not use `sd.stop()` or another inbuilt method here. Otherwise, it seems you can get PID and kill with `CTRL_C_EVENT` in windows: https://stackoverflow.com/questions/6645713/in-windows-killing-an-application-through-python – Ed Smith Nov 27 '18 at 11:24
  • @EdSmith I really appreciate your efforts. But it is not working and I am not sure whether I am doing it right :/ –  Nov 27 '18 at 11:36

3 Answers3

1

If the command

 exit()

make app doesn’t respond we can simulate ctrl event with

signal.CTRL_C_EVENT

in windows

And

signal.SIGINT

In Linux

REMEMBER TO IMPORT SIGNAL So the function become...

import signal
...
...
...
...

def end_button_func(self):
    signal.SIGINT # if you are using ubuntu or mac
    signal.CTRL_C_EVENT # if you are using windows

I have a Mac so I didn't try signal.CTRL_C_EVENT so try both anyway

Hope it will work!

Lorenzo Fiamingo
  • 3,251
  • 2
  • 17
  • 35
  • Using exit() in the end_button_func() makes the app to not respond. –  Nov 27 '18 at 10:48
  • Could you pls edit the part of my code accordingly? I have just replaced pass in the end_button_func to exit(). You can do the same, and try running. –  Nov 27 '18 at 10:51
  • At the moment i’m using SO iPhone app, because i’m at uni... if you won’t solve first i will try when i’ll come back home – Lorenzo Fiamingo Nov 27 '18 at 10:54
  • that would be great, Thanks. –  Nov 27 '18 at 11:00
  • Unfortunately, I couldn't solve the problem yet. It will be great if you can try to change the code with your answer. –  Nov 27 '18 at 13:20
  • Is it ok now? I am not been able to test it because every time I press start python crashes and I don't know why – Lorenzo Fiamingo Nov 27 '18 at 18:30
  • I guess you are missing libraries. Run the code via command prompt, so you can see what's wrong. –  Nov 28 '18 at 07:58
0

Looking at the source code of sounddevice, it looks like the KeyboardInterrupt event calls the exit method of argparser in their example for recording rec_unlimited.py,

parser = argparse.ArgumentParser(description=__doc__)

...

try:
    # Make sure the file is opened before recording anything:
    with sf.SoundFile(args.filename, mode='x', samplerate=args.samplerate,
                      channels=args.channels, subtype=args.subtype) as file:
        with sd.InputStream(samplerate=args.samplerate, device=args.device,
                            channels=args.channels, callback=callback):
            print('#' * 80)
            print('press Ctrl+C to stop the recording')
            print('#' * 80)
            while True:
                file.write(q.get())

except KeyboardInterrupt:
    print('\nRecording finished: ' + repr(args.filename))
    parser.exit(0)

Does this example work for you? I think exit here will simply call system exit anyway. If this works, then start from there and make sure your stop command in your GUI is doing the same thing (i.e. raising KeyboardInterrupt or exit).

It may also be an issue with using threading if sounddevice is creating threads (queue.get suggests this) so the exit call does not terminate all threads.

If this exit doesn't work in Windows, maybe calling sd.stop() may be a cross platform solution (although I suspect leaving the with block does this anyway).

UPDATE BASED ON DISCUSSION:

As the example works fine, but the end_button_func freezes the GUI, it seems a separate thread is needed for the record process so your GUI is responsive until a signal is passed to stop it. I think the best way is to pass an argument which triggers the KeyboardInterrupt exception in the thread when the stop button is pressed. To communicate with the thread, you need to send a signal as in this answer or this and based on that, raise the KeyboardInterrupt in the thread.

This example (PyQt: How to send a stop signal into a thread where an object is running a conditioned while loop?) seems the closest, where you'd adjust the work function to raise the exception as follows,

@pyqtSlot()
def work(self):
    while self.running():
        time.sleep(0.1)
        print 'doing work...'
    self.sgnFinished.emit()
    raise KeyboardInterrupt

I don't have PyQt5 or windows here so cannot test further. Note it seems you need to make the main class a QObject to use pyqtSignal.

Ed Smith
  • 12,716
  • 2
  • 43
  • 55
  • Yeah, the record function completely works fine, it stops the recording with a 'ctrl+c' press. But I am not able to integrate the start and end functionalities into the buttons. –  Nov 27 '18 at 12:20
  • 1
    @M.Denis Ok, so you want to recreate the action of ctrl+c, which creates a `KeyboardInterrupt` exception. What happens if you replace `pass` in `end_button_func` with `raise KeyboardInterrupt`? – Ed Smith Nov 27 '18 at 13:34
  • The problem is no matter what statement I give in the `end_button_func` the app freezes. Since, I am running the code via command prompt, if I go back to command prompt from the window, the 'ctrl+c' works and the app becomes normal. Because, the window is not looking for keyPressEvents that is why I have to go back to command prompt. –  Nov 27 '18 at 13:41
  • 1
    I guess you'd need to create a separate thread for your record process so your GUI is responsive and you can pass an argument which triggers the `KeyboardInterrupt` exception in the thread when the stop button is pressed. See this link https://www.pymadethis.com/article/multithreading-pyqt-applications-with-qthreadpool/ for information. Note you have a typo in `self.pushButton_1.clciked.connect` – Ed Smith Nov 27 '18 at 14:11
  • The threading got rid of the app freezing issue and made the `end_button_func` to work. Now, I have to generate the 'ctrl+c' keyboard interrupt in that function. –  Nov 27 '18 at 15:33
  • 1
    Excellent, note as each thread has its own context, passing an argument to the child thread and raising the `KeyboardInterrupt` exception IN the child thread based on that argument's value would be my suggested way to go. – Ed Smith Nov 27 '18 at 16:31
  • I updated the code implemented with threads. But I am not sure how to raise the `keyboardinterrupt` in the `end_button_func` to stop the recording. Bit more explanation could help me. –  Nov 28 '18 at 08:13
  • 1
    @M.Denis, did you fix this? I added a bit more detail in my answer to address your last comment. The problem is not simple, you may be best to strip out a minimum example of raising exception in a child process and ask a new question if you can't solve it. – Ed Smith Dec 01 '18 at 11:23
  • Sorry for the late reply. Yes, I fixed this problem. The following link shows a similar solution. https://stackoverflow.com/questions/53500124/how-to-catch-ctrlc-keypressevent-in-pyqt5-qwidget?noredirect=1#comment93878935_53500124 –  Jan 08 '19 at 08:28
  • Hi @M.Denis, great you fixed this but the attached link is to a removed question. Maybe worth adding an answer to this question above or accepting? – Ed Smith Jan 08 '19 at 15:48
0

You can do it like this fig. install the lib of OpenCV To use the cv2.waitKey() to control the program by keyboard.

enter image description here

Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
  • 1
    Please add code and data as text ([using code formatting](/editing-help#code)), not images. Images: A) don't allow us to copy-&-paste the code/errors/data for testing; B) don't permit searching based on the code/error/data contents; and [many more reasons](//meta.stackoverflow.com/a/285557). Images should only be used, in addition to text in code format, if having the image adds something significant that is not conveyed by just the text code/error/data. – Suraj Rao Apr 22 '22 at 05:44
  • 1
    Please use plain text in quotes, So we can copy paste it in our own environment for debugging. – niek tuytel Apr 22 '22 at 06:57