'What is the best way to generate 'Ctrl+c' event in python?

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


Solution 1:[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!

Solution 2:[2]

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.

Solution 3:[3]

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

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Suraj Rao