示例#1
0
 def _set_audio_device_box(self):
     # format: "idx - device name"
     devices = AudioDevice().list_devices()
     items = []
     for device in devices:
         index = device['index']
         name = device['name']
         item = str(index) + ' - ' + name
         items.append(item)
     self.ui.audioDeviceBox.addItems(items)
示例#2
0
    def __init__(self, parent=None):
        super().__init__(parent)

        # Default values. Updated if found in config.JSON
        self.use_qt_thread = False
        self.rhythm_algorithm = "multifeature"
        self.default_device_name = ""
        self.show_video_preview = True
        self.video_loop_bpm = 60
        self.video_update_skip_ms = 100
        self.limit_tempo_by_default = False
        self.tempo_lower_limit = 60.0
        self.tempo_upper_limit = 120.0
        self.screen = 0

        self.spotify_track_id = ""

        self.read_config()

        self.setWindowTitle("Gandalf Enjoys Music")
        self.desktop = QApplication.desktop()

        self.audio = AudioDevice(self.default_device_name)
        self.input_devices = self.audio.get_input_device_names()

        self.audio_changed.connect(self.audio.change_audio_input)

        if self.use_qt_thread:
            self.bpm_extractor = BPMQt(self.update_bpm,
                                       algorithm=self.rhythm_algorithm)
        else:
            self.bpm_extractor = BPMmp(self.update_bpm,
                                       algorithm=self.rhythm_algorithm)

        self.audio.data_ready.connect(self.bpm_extractor.start_bpm_calculation)

        self.init_ui()
示例#3
0
 def start_recording(self):
     self.file_path = self._get_save_path()
     # initialize stream
     audio_device_index = self._get_audio_device_index()
     for device in AudioDevice().list_devices():
         if device['index'] == audio_device_index:
             channels = device['channels']
     # TODO: Now I set `channels=1` to make the audio file to be mono.
     # It's only for the current audio interface. It's supposed to be
     # variable `channels`. Also it can be improved by specifying a certain
     # channel of input device, and always making the audio file mono.
     self.recording_file = Recorder(channels=1).open(
         self.file_path, audio_device_index)
     self.recording_file.start_recording()
     # update objects
     # update the buttons' status
     self.is_recording = True
     self._update_status()
     # clean up any text in promptLabel
     self.ui.promptLabel.setText('')
     # start counting
     self.timer.start(1000)
import matplotlib.animation
import numpy
import threading

# engine = engine_factory.v_four_90_deg()
# engine = engine_factory.w_16()
# engine = engine_factory.v_8_LS()
# engine = engine_factory.inline_5_crossplane()
# engine = engine_factory.inline_6()
engine = engine_factory.boxer_4_crossplane_custom(
    [1, 1, 0, 0])  # (rando := random.randrange(360)))
# engine = engine_factory.boxer_4_half()
# engine = engine_factory.random()
# engine = engine_factory.fake_rotary_2rotor()

audio_device = AudioDevice()
stream = audio_device.play_stream(engine.gen_audio)

print('\nEngine is running...')
# print(rando)

RATE = 44100
BUFFER = 882

fig = plt.figure()
line1 = plt.plot([], [])[0]
line2 = plt.plot([], [])[0]

r = range(0, int(RATE / 2 + 1), int(RATE / BUFFER))
l = len(r)
    def __init__(self, logger):
        self.logger = logger
        super(MainWindow, self).__init__()
        self.ui = Terzpegelmesser.Ui_MainWindow()
        self.ui.setupUi(self)
        self.chunk_number = 0
        self.buffer_timer_time = 0.
        self.cpu_percent = 0.
        self.setMinimumSize(1000, 600)
        # Initialize the audio data ring buffer
        self.audiobuffer = AudioBuffer(self.logger)
        # Initialize the audio device
        self.audio_device = AudioDevice(self.logger)
        # Initialize the blocklength
        self.blocklength = 2048
        self.ui.BoxFFT.setCurrentIndex(6)
        self.logger.push("initial set Blocksize to " + str(self.blocklength))
        # Initialize the frequency weighting flag
        self.weight = 0
        # Initialize the number of samples shown in waveform monitor
        self.window = 128
        # Initialize the number of periods shown in waveform monitor
        self.NumberOfPeriods = 10
        # Initialize the flag for lin (0) and log (1) fft plotting
        self.plotflag = 1
        devices = self.audio_device.get_readable_devices_list()

        for device in devices:
            self.ui.DeviceList.addItem(device)

        current_device = self.audio_device.get_readable_current_device()
        self.ui.DeviceList.setCurrentIndex(current_device)
        self.display_timer = QTimer()
        self.display_timer.setInterval(SMOOTH_DISPLAY_TIMER_PERIOD_MS)
        self.connect(self.display_timer, SIGNAL('timeout()'),
                      self.update_buffer)
        self.connect(self.ui.ButtonStartStop, SIGNAL('triggered()'),
                     self.stream_run)
        self.connect(self.ui.DeviceList, SIGNAL('currentIndexChanged(int)'),
                     self.input_device_changed)
        self.connect(self.ui.BoxFFT, SIGNAL('currentIndexChanged(int)'),
                      self.update_blocklength)

        self.ui.action32.triggered.connect(lambda: self.update_blocklength(0))
        self.ui.action32.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(0))
        self.ui.action64.triggered.connect(lambda: self.update_blocklength(1))
        self.ui.action64.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(1))
        self.ui.action128.triggered.connect(lambda: self.update_blocklength(2))
        self.ui.action128.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(2))
        self.ui.action256.triggered.connect(lambda: self.update_blocklength(3))
        self.ui.action256.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(3))
        self.ui.action512.triggered.connect(lambda: self.update_blocklength(4))
        self.ui.action512.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(4))
        self.ui.action1024.triggered.connect(lambda: self.update_blocklength(
            5))
        self.ui.action1024.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(5))
        self.ui.action2048.triggered.connect(lambda: self.update_blocklength(
            6))
        self.ui.action2048.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(6))
        self.ui.action4096.triggered.connect(lambda: self.update_blocklength(
            7))
        self.ui.action4096.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(7))
        self.ui.action8192.triggered.connect(lambda: self.update_blocklength(
            8))
        self.ui.action8192.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(8))

        self.ui.actionNone.triggered.connect(lambda: self.update_weight(0))
        self.ui.actionNone.triggered.connect(
            lambda: self.ui.BoxBew.setCurrentIndex(0))
        self.ui.actionA.triggered.connect(lambda: self.update_weight(1))
        self.ui.actionA.triggered.connect(
            lambda: self.ui.BoxBew.setCurrentIndex(1))
        self.ui.actionC.triggered.connect(lambda: self.update_weight(2))
        self.ui.actionC.triggered.connect(
            lambda: self.ui.BoxBew.setCurrentIndex(2))

        self.connect(self.ui.BoxBew, SIGNAL('currentIndexChanged(int)'),
                      self.update_weight)
        self.connect(self.ui.RadioLin, SIGNAL("clicked()"),
                      self.update_plotflag_lin)
        self.connect(self.ui.RadioLog, SIGNAL("clicked()"),
                      self.update_plotflag_log)

        self.ui.actionLogarithmic.triggered.connect(self.update_plotflag_log)
        self.ui.actionLogarithmic.triggered.connect(
            lambda: self.ui.RadioLog.setChecked(True))
        self.ui.actionLinear.triggered.connect(self.update_plotflag_lin)
        self.ui.actionLinear.triggered.connect(
            lambda: self.ui.RadioLin.setChecked(True))

        self.connect(self.ui.push_plus, SIGNAL("clicked()"),
                      self.update_NumberOfPeriods_minus)
        self.ui.actionZoom_Out.triggered.connect(
                    self.update_NumberOfPeriods_plus)

        self.connect(self.ui.push_minus, SIGNAL("clicked()"),
                      self.update_NumberOfPeriods_plus)
        self.ui.actionZoom_In.triggered.connect(
                    self.update_NumberOfPeriods_minus)

        self.gain_plotter = (
                        gain_plotter.Gain_Plotter(self.ui.PlotGainVerlauf,
                                                       self.audiobuffer))
        self.spektro_plotter = (
                third_octave_plotter.SpektroPlotter(self.ui.PlotTerzpegel,
                                                            self.audiobuffer))
        self.waveform = waveform.Oszi(self.ui.PlotWellenform,
                                      self.audiobuffer, self.NumberOfPeriods)
        self.channelplotter = (
                        channel_plotter.ChannelPlotter(self.ui.PlotKanalpegel,
                                                             self.audiobuffer))
        self.specgramplot = (
                spectrogram_plotter.Spectrogram_Plot(self.ui.PlotSpektrogramm,
                                                            self.audiobuffer))
        self.spektro_plotter_2 = (
                third_octave_plotter.SpektroPlotter(self.ui.PlotTerzpegel_2,
                                                            self.audiobuffer))
        self.fft_plot = fft_plotter.FFTPlotter(self.ui.PlotFFT,
                                               self.audiobuffer,
                                               self.blocklength, self.plotflag)
    # if the startStop button is clicked, the timer starts and the stream is
    # filled with acoustic data
        self.ui.ButtonStartStop.clicked.connect(self.stream_run)
        self.ui.ButtonStartStop.state = 0
        self.display_timer.timeout.connect(self.update_plot)
class MainWindow(QMainWindow):
    def __init__(self, logger):
        self.logger = logger
        super(MainWindow, self).__init__()
        self.ui = Terzpegelmesser.Ui_MainWindow()
        self.ui.setupUi(self)
        self.chunk_number = 0
        self.buffer_timer_time = 0.
        self.cpu_percent = 0.
        self.setMinimumSize(1000, 600)
        # Initialize the audio data ring buffer
        self.audiobuffer = AudioBuffer(self.logger)
        # Initialize the audio device
        self.audio_device = AudioDevice(self.logger)
        # Initialize the blocklength
        self.blocklength = 2048
        self.ui.BoxFFT.setCurrentIndex(6)
        self.logger.push("initial set Blocksize to " + str(self.blocklength))
        # Initialize the frequency weighting flag
        self.weight = 0
        # Initialize the number of samples shown in waveform monitor
        self.window = 128
        # Initialize the number of periods shown in waveform monitor
        self.NumberOfPeriods = 10
        # Initialize the flag for lin (0) and log (1) fft plotting
        self.plotflag = 1
        devices = self.audio_device.get_readable_devices_list()

        for device in devices:
            self.ui.DeviceList.addItem(device)

        current_device = self.audio_device.get_readable_current_device()
        self.ui.DeviceList.setCurrentIndex(current_device)
        self.display_timer = QTimer()
        self.display_timer.setInterval(SMOOTH_DISPLAY_TIMER_PERIOD_MS)
        self.connect(self.display_timer, SIGNAL('timeout()'),
                      self.update_buffer)
        self.connect(self.ui.ButtonStartStop, SIGNAL('triggered()'),
                     self.stream_run)
        self.connect(self.ui.DeviceList, SIGNAL('currentIndexChanged(int)'),
                     self.input_device_changed)
        self.connect(self.ui.BoxFFT, SIGNAL('currentIndexChanged(int)'),
                      self.update_blocklength)

        self.ui.action32.triggered.connect(lambda: self.update_blocklength(0))
        self.ui.action32.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(0))
        self.ui.action64.triggered.connect(lambda: self.update_blocklength(1))
        self.ui.action64.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(1))
        self.ui.action128.triggered.connect(lambda: self.update_blocklength(2))
        self.ui.action128.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(2))
        self.ui.action256.triggered.connect(lambda: self.update_blocklength(3))
        self.ui.action256.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(3))
        self.ui.action512.triggered.connect(lambda: self.update_blocklength(4))
        self.ui.action512.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(4))
        self.ui.action1024.triggered.connect(lambda: self.update_blocklength(
            5))
        self.ui.action1024.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(5))
        self.ui.action2048.triggered.connect(lambda: self.update_blocklength(
            6))
        self.ui.action2048.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(6))
        self.ui.action4096.triggered.connect(lambda: self.update_blocklength(
            7))
        self.ui.action4096.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(7))
        self.ui.action8192.triggered.connect(lambda: self.update_blocklength(
            8))
        self.ui.action8192.triggered.connect(
            lambda: self.ui.BoxFFT.setCurrentIndex(8))

        self.ui.actionNone.triggered.connect(lambda: self.update_weight(0))
        self.ui.actionNone.triggered.connect(
            lambda: self.ui.BoxBew.setCurrentIndex(0))
        self.ui.actionA.triggered.connect(lambda: self.update_weight(1))
        self.ui.actionA.triggered.connect(
            lambda: self.ui.BoxBew.setCurrentIndex(1))
        self.ui.actionC.triggered.connect(lambda: self.update_weight(2))
        self.ui.actionC.triggered.connect(
            lambda: self.ui.BoxBew.setCurrentIndex(2))

        self.connect(self.ui.BoxBew, SIGNAL('currentIndexChanged(int)'),
                      self.update_weight)
        self.connect(self.ui.RadioLin, SIGNAL("clicked()"),
                      self.update_plotflag_lin)
        self.connect(self.ui.RadioLog, SIGNAL("clicked()"),
                      self.update_plotflag_log)

        self.ui.actionLogarithmic.triggered.connect(self.update_plotflag_log)
        self.ui.actionLogarithmic.triggered.connect(
            lambda: self.ui.RadioLog.setChecked(True))
        self.ui.actionLinear.triggered.connect(self.update_plotflag_lin)
        self.ui.actionLinear.triggered.connect(
            lambda: self.ui.RadioLin.setChecked(True))

        self.connect(self.ui.push_plus, SIGNAL("clicked()"),
                      self.update_NumberOfPeriods_minus)
        self.ui.actionZoom_Out.triggered.connect(
                    self.update_NumberOfPeriods_plus)

        self.connect(self.ui.push_minus, SIGNAL("clicked()"),
                      self.update_NumberOfPeriods_plus)
        self.ui.actionZoom_In.triggered.connect(
                    self.update_NumberOfPeriods_minus)

        self.gain_plotter = (
                        gain_plotter.Gain_Plotter(self.ui.PlotGainVerlauf,
                                                       self.audiobuffer))
        self.spektro_plotter = (
                third_octave_plotter.SpektroPlotter(self.ui.PlotTerzpegel,
                                                            self.audiobuffer))
        self.waveform = waveform.Oszi(self.ui.PlotWellenform,
                                      self.audiobuffer, self.NumberOfPeriods)
        self.channelplotter = (
                        channel_plotter.ChannelPlotter(self.ui.PlotKanalpegel,
                                                             self.audiobuffer))
        self.specgramplot = (
                spectrogram_plotter.Spectrogram_Plot(self.ui.PlotSpektrogramm,
                                                            self.audiobuffer))
        self.spektro_plotter_2 = (
                third_octave_plotter.SpektroPlotter(self.ui.PlotTerzpegel_2,
                                                            self.audiobuffer))
        self.fft_plot = fft_plotter.FFTPlotter(self.ui.PlotFFT,
                                               self.audiobuffer,
                                               self.blocklength, self.plotflag)
    # if the startStop button is clicked, the timer starts and the stream is
    # filled with acoustic data
        self.ui.ButtonStartStop.clicked.connect(self.stream_run)
        self.ui.ButtonStartStop.state = 0
        self.display_timer.timeout.connect(self.update_plot)

    def update_plot(self):

        isvis_FFT = self.ui.PlotFFT.isVisible()
        self.channelplotter.plot()
        self.gain_plotter.plot()
        self.spektro_plotter.plot(self.weight)
        self.spektro_plotter_2.plot(self.weight)
        self.waveform.plot(self.NumberOfPeriods)
        if isvis_FFT == False:
            self.specgramplot.plotspecgram()

        if isvis_FFT == True:
            self.fft_plot.plot(self.blocklength, self.plotflag)

    # opens stream if there is none, else closes it
    def stream_run(self):

        if self.ui.ButtonStartStop.state == 0:
            #openstream()
            self.logger.push("Timer start")
            self.display_timer.start()
            self.ui.ButtonStartStop.setText("Stop")
            self.display_timer.start()
            print(logger.log)
            self.ui.ButtonStartStop.state = 1
        else:
            #closestream()
            self.logger.push("Timer stop")
            self.display_timer.stop()
            self.ui.ButtonStartStop.setText("Start")
            self.display_timer.stop()
            self.ui.ButtonStartStop.state = 0
            print(logger.log)

    def update_buffer(self):
        chunks, t, newpoints = (
                        self.audio_device.update(self.audiobuffer.ringbuffer))
        self.audiobuffer.set_newdata(newpoints)
        self.chunk_number += chunks
        self.buffer_timer_time = (95. * self.buffer_timer_time + 5. * t) / 100.

    def update_blocklength(self, newblocklength):
        self.blocklength = 32 * (2 ** newblocklength)
        self.logger.push("Blocksize changed to " + str(self.blocklength))
        print(logger.log)
        self.fft_plot.must_plot = True

    def update_NumberOfPeriods_plus(self):
        self.NumberOfPeriods += 1
        self.logger.push("Desired number of periods: " +
                         str(self.NumberOfPeriods))
        print(logger.log)

    def update_NumberOfPeriods_minus(self):
        self.NumberOfPeriods -= 1

        # sets lower limit of self.NumberOfPeriods to 1
        if self.NumberOfPeriods < 1:
            self.NumberOfPeriods = 1

        self.logger.push("Desired number of periods: " +
                         str(self.NumberOfPeriods))
        print(logger.log)

    def update_weight(self, weight):
        self.weight = weight
        if self.weight == 0:
            self.logger.push("Using Z Curve (unweighted)")
            print(logger.log)
        elif self.weight == 1:
            self.logger.push("Using A Curve")
            print(logger.log)
        elif self.weight == 2:
            self.logger.push("Using C Curve")
            print(logger.log)
        else:
            print self.weight

    def update_plotflag_lin(self):
        self.plotflag = 0
        self.logger.push("Linear frequency axis selected")
        self.fft_plot.must_plot = True
        print(logger.log)

    def update_plotflag_log(self):
        self.plotflag = 1
        self.logger.push("Logarithmic frequency axis selected")
        self.fft_plot.must_plot = True
        print(logger.log)

    def input_device_changed(self, index):
        success, index = self.audio_device.select_input_device(index)
        self.ui.DeviceList.setCurrentIndex(index)
        self.fft_plot.must_plot = True
        if not success:
# Note: the error message is a child of the settings dialog, so that
# that dialog remains on top when the error message is closed
            error_message = QErrorMessage(self.settings_dialog)
            error_message.setWindowTitle("Input device error")
            error_message.showMessage("Impossible to use the selected input"
                                      " device, reverting to the previous one")

    def statistics(self):
        if not self.about_dialog.LabelStats.isVisible():
            return
        label = "Chunk #%d\n"\
        "Audio buffer retrieval: %.02f ms\n"\
        "Global CPU usage: %d %%\n"\
        "Number of overflowed inputs (XRUNs): %d"\
        % (self.chunk_number, self.buffer_timer_time, self.cpu_percent,
            self.audio_device.xruns)
        self.about_dialog.LabelStats.setText(label)
示例#7
0
class MainWindow(QMainWindow):
    """Display video loop and controls"""
    audio_changed = Signal(str)
    def __init__(self, parent=None):
        super().__init__(parent)

        # Default values. Updated if found in config.JSON
        self.use_qt_thread = False
        self.rhythm_algorithm = "multifeature"
        self.default_device_name = ""
        self.show_video_preview = True
        self.video_loop_bpm = 60
        self.video_update_skip_ms = 100
        self.limit_tempo_by_default = False
        self.tempo_lower_limit = 60.0
        self.tempo_upper_limit = 120.0
        self.screen = 0

        self.spotify_track_id = ""

        self.read_config()

        self.setWindowTitle("Gandalf Enjoys Music")
        self.desktop = QApplication.desktop()

        self.audio = AudioDevice(self.default_device_name)
        self.input_devices = self.audio.get_input_device_names()

        self.audio_changed.connect(self.audio.change_audio_input)

        if self.use_qt_thread:
            self.bpm_extractor = BPMQt(self.update_bpm,
                                       algorithm=self.rhythm_algorithm)
        else:
            self.bpm_extractor = BPMmp(self.update_bpm,
                                       algorithm=self.rhythm_algorithm)

        self.audio.data_ready.connect(self.bpm_extractor.start_bpm_calculation)

        self.init_ui()

    def init_ui(self):
        dir_path = os.path.dirname(os.path.realpath(__file__))
        file_location = dir_path + "/resources/gandalf_icon_256px.png"
        self.icon_pixmap = QPixmap(file_location)
        self.icon = QIcon(self.icon_pixmap)
        self.setWindowIcon(self.icon)
        self.setWindowIconText("Gandalf Enjoys Music")

        self.central = QWidget(self)
        self.setCentralWidget(self.central)
        self.layout = QVBoxLayout()

        self.lock_checkbox = QCheckBox("Manual tempo", self)
        self.lock_checkbox.clicked.connect(self.update_lock_checkbox)

        self.limit_layout = QVBoxLayout()
        self.limit_checkbox = QCheckBox("Limit tempo between:", self)
        self.limit_checkbox.setChecked(self.limit_tempo_by_default)
        self.limit_checkbox.clicked.connect(self.update_bpm_manually)

        self.init_video()

        if self.show_video_preview:
            self.setFixedSize(QSize(500, 350))
            self.layout.addWidget(self.video_widget)
        else:
            self.setFixedSize(500, 100)
            self.fullscreen_button = QPushButton(self)
            self.fullscreen_button.setText("Go Fullscreen")
            self.layout.addWidget(self.fullscreen_button)
            self.fullscreen_button.clicked.connect(self.show_fullscreen)
            self.video_widget.fullscreen_changed.connect(
                self.update_button_text)

        self.video_widget.fullscreen_changed.connect(
            self.reset_video_position
        )

        self.tempo_control_layout = QVBoxLayout()
        self.tempo_control_layout.addWidget(self.lock_checkbox)

        self.set_bpm_widget = QLineEdit("{:.1f}".format(self.old_bpm), self)
        self.set_bpm_widget.setMaxLength(5)
        self.set_bpm_widget.returnPressed.connect(self.update_bpm_manually)
        self.set_bpm_palette = QPalette()
        self.set_bpm_palette.setColor(QPalette.Text, Qt.gray)
        self.set_bpm_widget.setPalette(self.set_bpm_palette)
        self.set_bpm_widget.setFixedWidth(50)
        self.tempo_control_layout.addWidget(self.set_bpm_widget)

        self.limit_layout.addWidget(self.limit_checkbox)

        self.limits = QHBoxLayout()

        self.lower_bpm_widget = QLineEdit(str(self.tempo_lower_limit), self)
        self.lower_bpm_widget.setMaxLength(5)
        self.lower_bpm_widget.returnPressed.connect(self.update_lower_limit)
        self.lower_bpm_widget.setFixedWidth(50)
        self.limits.addWidget(self.lower_bpm_widget)

        self.upper_bpm_widget = QLineEdit(str(self.tempo_upper_limit), self)
        self.upper_bpm_widget.setMaxLength(5)
        self.upper_bpm_widget.returnPressed.connect(self.update_upper_limit)
        self.upper_bpm_widget.setFixedWidth(50)
        self.limits.addWidget(self.upper_bpm_widget)
        self.limit_layout.addLayout(self.limits)

        self.control_layout = QHBoxLayout()
        self.control_layout.addLayout(self.tempo_control_layout)
        self.control_layout.addLayout(self.limit_layout)

        self.save_settings_button = QPushButton("Save settings", self)
        self.save_settings_button.clicked.connect(self.save_config)
        self.control_layout.addWidget(self.save_settings_button)

        self.layout.addLayout(self.control_layout)

        self.device_layout = QHBoxLayout()
        self.audio_select_label = QLabel("Audio device:", self)
        self.device_layout.addWidget(self.audio_select_label)

        self.audio_selection = QComboBox(self)
        self.audio_selection.addItems(self.input_devices)
        self.audio_selection.currentIndexChanged.connect(self.audio_selection_changed)
        self.device_layout.addWidget(self.audio_selection)

        self.layout.addLayout(self.device_layout)

        self.central.setLayout(self.layout)

    def init_video(self):
        self.old_bpm = 1.0

        self.video_widget = VideoWidget(self,
                                        self.show_video_preview,
                                        self.screen)
        self.media_player = QMediaPlayer(self.central)
        self.media_player.setVideoOutput(self.video_widget)

        self.playlist = QMediaPlaylist(self.media_player)
        dir_path = os.path.dirname(os.path.realpath(__file__))
        file_location = dir_path + "/resources/video_long.mp4"
        self.video_file = QUrl.fromLocalFile(file_location)
        self.playlist.addMedia(self.video_file)
        self.playlist.setPlaybackMode(QMediaPlaylist.Loop)
        self.playlist.setCurrentIndex(0)
        self.media_player.setPlaylist(self.playlist)
        self.media_player.mediaStatusChanged.connect(self.handle_media_state_changed)

        self.media_player.play()

        self.change_playback_rate(self.video_loop_bpm)

        if not self.show_video_preview:
            self.video_widget.hide()

    def handle_media_state_changed(self, state):
        if state == QMediaPlayer.MediaStatus.BufferedMedia:
            playback_speed = self.old_bpm / self.video_loop_bpm
            self.media_player.setPlaybackRate(playback_speed)
            self.media_player.setPosition(0)

    def change_playback_rate(self, bpm):
        """Update playback speed for video loop."""
        if bpm != self.old_bpm:
            # Prevent switching between double and half tempo during the same song in spotify
            track_id = get_spotify_track()
            if not self.lock_checkbox.isChecked()\
                    and not self.limit_checkbox.isChecked()\
                    and (math.isclose(bpm*2,self.old_bpm, rel_tol=3e-2)\
                    or math.isclose(bpm, self.old_bpm*2, rel_tol=3e-2))\
                    and track_id and track_id == self.spotify_track_id:
                self.spotify_track_id = track_id
                return
            self.spotify_track_id = track_id
            
            self.old_bpm = bpm
            playback_speed = bpm / self.video_loop_bpm

            # Workaround for a bug which causes irregular video playback speed
            # after changing playback rate
            current_position = self.media_player.position()
            self.media_player.setPlaybackRate(playback_speed)
            self.media_player.setPosition(current_position
                                          + self.video_update_skip_ms
                                          * playback_speed)

    def update_bpm(self, bpm, manual=False):
        if not manual:
            if self.lock_checkbox.isChecked():
                return
            bpm = float(int(bpm+0.5))
        if self.limit_checkbox.isChecked():
            while bpm < self.tempo_lower_limit:
                bpm = bpm * 2.0
            while bpm > self.tempo_upper_limit:
                bpm = bpm / 2.0
        self.change_playback_rate(bpm)
        self.set_bpm_widget.setText("{:.1f}".format(self.old_bpm))

    def update_bpm_manually(self):
        bpm = self.set_bpm_widget.text()
        try:
            bpm = float(bpm)
            if bpm < 1.0:
                raise ValueError
        except ValueError:
            return
        self.spotify_track_id = ""
        self.update_bpm(bpm, manual=True)

    def update_lock_checkbox(self):
        if self.lock_checkbox.isChecked():
            self.set_bpm_palette = QPalette()
            self.set_bpm_palette.setColor(QPalette.Text, Qt.black)
            self.set_bpm_widget.setPalette(self.set_bpm_palette)
            self.set_bpm_widget.setReadOnly(False)
        else:
            self.set_bpm_palette = QPalette()
            self.set_bpm_palette.setColor(QPalette.Text, Qt.gray)
            self.set_bpm_widget.setPalette(self.set_bpm_palette)
            self.set_bpm_widget.setReadOnly(True)

    def update_lower_limit(self, value=None):
        if not value:
            value = self.lower_bpm_widget.text()
        try:
            value = float(value)
            if value < 1.0:
                raise ValueError
        except ValueError:
            return
        if value <= self.tempo_upper_limit / 2.0:
            self.tempo_lower_limit = value
        else:
            self.tempo_lower_limit = self.tempo_upper_limit / 2.0
        self.lower_bpm_widget.setText("{:.1f}".format(self.tempo_lower_limit))

    def update_upper_limit(self, value=None):
        if not value:
            value = self.upper_bpm_widget.text()
        try:
            value = float(value)
            if value < 1.0:
                raise ValueError
        except ValueError:
            return
        if value >= self.tempo_lower_limit * 2.0:
            self.tempo_upper_limit = value
        else:
            self.tempo_upper_limit = self.tempo_lower_limit * 2.0
        self.upper_bpm_widget.setText("{:.1f}".format(self.tempo_upper_limit))

    def audio_selection_changed(self, idx):
        self.audio_changed.emit(self.audio_selection.currentText())

    @Slot()
    def show_fullscreen(self):
        self.reset_video_position()
        if self.video_widget.isFullScreen():
            self.video_widget.hide()
            self.fullscreen_button.setText("Go Fullscreen")
        else:
            self.video_widget.setFullScreen(True)
            self.video_widget.setGeometry(self.desktop.screenGeometry(self.screen))
            self.fullscreen_button.setText("Hide Fullscreen")

    @Slot()
    def reset_video_position(self):
        self.media_player.setPosition(0)

    @Slot(bool)
    def update_button_text(self, fullscreen_status):
        if fullscreen_status:
            self.fullscreen_button.setText("Hide Fullscreen")
        else:
            self.fullscreen_button.setText("Go Fullscreen")

    def read_config(self):
        with open("config.JSON") as config_file:
            config = json.load(config_file)

            if "no_multiprocess" in config:
                self.use_qt_thread = config["no_multiprocess"]
            if config.get("rhythm_algorithm_faster"):
                self.rhythm_algorithm = "degara"
            if config.get("default_device"):
                self.default_device_name = config["default_device"]
            if "show_video_preview" in config:
                self.show_video_preview = config.get("show_video_preview")
            if config.get("video_loop_bpm"):
                self.video_loop_bpm = config["video_loop_bpm"]
            if config.get("video_update_skip_time_ms"):
                self.video_update_skip_ms = config["video_update_skip_time_ms"]
            if config.get("limit_tempo_by_default"):
                self.limit_tempo_by_default = config["limit_tempo_by_default"]
            if config.get("tempo_lower_limit"):
                self.tempo_lower_limit = config["tempo_lower_limit"]
            if config.get("tempo_upper_limit"):
                self.tempo_upper_limit = config["tempo_upper_limit"]
            if "screen" in config:
                self.screen = config["screen"]

    @Slot()
    def save_config(self):
        fast_rhythm_algo = self.rhythm_algorithm == "degara"
        data = {
            "no_multiprocess": self.use_qt_thread,
            "rhythm_algorithm_faster": fast_rhythm_algo,
            "default_device": self.audio_selection.currentText(),
            "show_video_preview": self.show_video_preview,
            "video_loop_bpm": self.video_loop_bpm,
            "video_update_skip_time_ms": self.video_update_skip_ms,
            "limit_tempo_by_default": self.limit_checkbox.isChecked(),
            "tempo_lower_limit": self.tempo_lower_limit,
            "tempo_upper_limit": self.tempo_upper_limit,
            "screen": self.screen
        }
        with open("config.JSON", "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=4)