예제 #1
0
    def __init__(self, startupDelay: float) -> None:
        threading.Thread.__init__(self)
        self.startupDelay = startupDelay
        self.finish = False
        if isMac and qtminor > 12:
            # trigger permission prompt
            from PyQt5.QtMultimedia import QAudioDeviceInfo

            QAudioDeviceInfo.defaultInputDevice()
예제 #2
0
파일: sound.py 프로젝트: shaunren/anki
    def __init__(self, output_path: str, startup_delay: float) -> None:
        threading.Thread.__init__(self)
        self._output_path = output_path
        self._startup_delay = startup_delay
        self.finish = False
        # though we're using pyaudio here, we rely on Qt to trigger
        # the permission prompt on macOS
        if isMac and qtminor > 12:
            from PyQt5.QtMultimedia import QAudioDeviceInfo

            QAudioDeviceInfo.defaultInputDevice()
예제 #3
0
파일: sound.py 프로젝트: shaunren/anki
    def __init__(self, output_path: str, mw: aqt.AnkiQt,
                 parent: QWidget) -> None:
        super().__init__(output_path)

        self.mw = mw
        self._parent = parent

        from PyQt5.QtMultimedia import QAudioDeviceInfo, QAudioFormat, QAudioInput

        format = QAudioFormat()
        format.setChannelCount(1)
        format.setSampleRate(44100)
        format.setSampleSize(16)
        format.setCodec("audio/pcm")
        format.setByteOrder(QAudioFormat.LittleEndian)
        format.setSampleType(QAudioFormat.SignedInt)

        device = QAudioDeviceInfo.defaultInputDevice()
        if not device.isFormatSupported(format):
            format = device.nearestFormat(format)
            print("format changed")
            print("channels", format.channelCount())
            print("rate", format.sampleRate())
            print("size", format.sampleSize())
        self._format = format

        self._audio_input = QAudioInput(device, format, parent)
예제 #4
0
 def initAUD(self):
     #
     info = QAudioDeviceInfo.defaultInputDevice()
     if (~info.isFormatSupported(self.audio.format)):
         # print("警告,设置的默认音频格式并不支持,将尝试采用最相近的支持格式")
         # 不知道这里面有什么神改动?
         self.audio.format  = info.nearestFormat(self.audio.format)
     #
     update_interval = 160
     self.audioRecorder = QAudioInput(self.audio.format)
     self.audioRecorder.setNotifyInterval(update_interval) #按毫秒ms 类似于QTimer的作用
     self.audioRecorder.notify.connect(self.processAudioData)
     self.audioRecorder_TD = QThread()
     self.audioRecorder.moveToThread(self.audioRecorder_TD)
     self.audioRecorder_TD.started.connect(self.startRecord)
     self.audioRecorder.stateChanged.connect(self.recordStopped)
     # 总结来说线程只是一个容器,里面执行的循环要是没法结束,强制退出也不好操作
     # 所以还是好好写好任务流然后发送信号比较合理
     self.audioPlayer = QAudioOutput(self.audio.format)
     self.audioPlayer.setNotifyInterval(update_interval)
     self.audioPlayer.notify.connect(self.processAudioData)
     self.audioPlayer_TD = QThread()
     self.audioPlayer.moveToThread(self.audioPlayer_TD)
     self.audioPlayer_TD.started.connect(self.startPlay)
     self.audioPlayer.stateChanged.connect(self.playStopped)
    def __init__(self):
        super().__init__()

        self.m_chart = QChart()

        chart_view = QChartView(self.m_chart)
        chart_view.setMinimumSize(800, 600)

        self.m_series = QLineSeries()
        self.m_chart.addSeries(self.m_series)

        axis_x = QValueAxis()
        axis_x.setRange(0, 2000)
        axis_x.setLabelFormat("%g")
        axis_x.setTitleText("Samples")

        axis_y = QValueAxis()
        axis_y.setRange(-1, 1)
        axis_y.setTitleText("Audio level")

        self.m_chart.setAxisX(axis_x, self.m_series)
        self.m_chart.setAxisY(axis_y, self.m_series)
        self.m_chart.setTitle("Data from the microphone")

        main_layout = QVBoxLayout()
        main_layout.addWidget(chart_view)
        self.setLayout(main_layout)

        format_audio = QAudioFormat()
        format_audio.setSampleRate(48000)
        format_audio.setChannelCount(1)
        format_audio.setSampleSize(8)
        format_audio.setCodec("audio/pcm")
        format_audio.setByteOrder(QAudioFormat.LittleEndian)
        format_audio.setSampleType(QAudioFormat.UnSignedInt)

        input_devices = QAudioDeviceInfo.defaultInputDevice()
        self.m_audio_input = QAudioInput(input_devices, format_audio)

        self.m_device = XYSeriesIODevice(self.m_series)
        self.m_device.open(QIODevice.WriteOnly)

        self.m_audio_input.start(self.m_device)

        self.init_ui()
예제 #6
0
    def _uic(self):
        """
    set up our layout which consists of:

                 Big Honkin' Label
        [input combobox]    [output combobox]
               [volume slider]  [x] Mute

    hooking put the signals to useful slots is the job
    of __init__. Here just make the layout.
        """
        self.setMinimumWidth(400)
        # Create the big honkin' label and logo
        icon_pixmap = QPixmap(":/icon.png").scaledToWidth(64)
        icon_label = QLabel()
        icon_label.setPixmap(icon_pixmap)
        text_label = QLabel("Sidetone!")
        hb_label = QHBoxLayout()
        hb_label.addStretch(1)
        hb_label.addWidget(icon_label, 0)
        hb_label.addWidget(text_label, 0)
        hb_label.addStretch(1)

        # Create a list of QAudioInfo objects for inputs
        self.input_info_list = QAudioDeviceInfo.availableDevices(QAudio.AudioInput)
        if 0 == len(self.input_info_list):
            self.input_info_list = [QAudioDeviceInfo.defaultInputDevice()]
        # Create a combo box and populate it with names of inputs
        self.cb_inputs = QComboBox()
        self.cb_inputs.addItems([audio_info.deviceName() for audio_info in self.input_info_list])
        # Create a list of QAudioInfo objects for outputs
        self.otput_info_list = QAudioDeviceInfo.availableDevices(QAudio.AudioOutput)
        if 0 == len(self.otput_info_list):
            self.otput_info_list = [QAudioDeviceInfo.defaultOutputDevice()]
        self.status_bar.showMessage(
            "{} inputs {} otputs".format(len(self.input_info_list), len(self.otput_info_list)), 2000
        )
        # Create a combo box and populate it with names of outputs
        self.cb_otputs = QComboBox()
        self.cb_otputs.addItems([audio_info.deviceName() for audio_info in self.otput_info_list])
        # Lay those two out aligned to the outside
        hb_combos = QHBoxLayout()
        hb_combos.addWidget(self.cb_inputs, 1)
        hb_combos.addStretch(0)
        hb_combos.addWidget(self.cb_otputs, 1)
        # Create a volume slider from 0 to 100.
        self.volume = QSlider(Qt.Horizontal, self)
        self.volume.setMinimum(0)
        self.volume.setMaximum(100)
        self.volume.setTickInterval(10)
        self.volume.setTickPosition(QSlider.TicksBothSides)
        # Create a checkbox "Mute"
        self.mute = QCheckBox("Mute")
        # Put those together in a row squeezed in the center
        hb_volume = QHBoxLayout()
        hb_volume.addStretch(1)
        hb_volume.addWidget(self.volume, 1)
        hb_volume.addWidget(self.mute, 0)
        hb_volume.addStretch(1)
        # Stack all those up as this widget's layout
        vlayout = QVBoxLayout()
        vlayout.addLayout(hb_label)
        vlayout.addLayout(hb_combos)
        vlayout.addLayout(hb_volume)
        self.setLayout(vlayout)
예제 #7
0
    def initUI(self):
        
        # main window/layout
        window = QWidget()
        layout = QVBoxLayout()
        
        # layout for audio device and sample rate selection
        deviceLayout = QHBoxLayout()
        
        # make audio device selection box and list of available devices
        self.deviceBox = QComboBox()
        defaultDeviceInfo = QAudioDeviceInfo.defaultInputDevice()
        self.availableDevices = [defaultDeviceInfo]
        self.availableDevices += QAudioDeviceInfo.availableDevices(
                                                             QAudio.AudioInput)
        for device in self.availableDevices:
            self.deviceBox.addItem(device.deviceName())
            
        # make sample rate label and combobox
        sRateLabel = QLabel("Sample rate:")
        sRateLabel.setAlignment(Qt.AlignRight)
        
        # user can choose between 44.1 and 48kHz (valid DetectorBank rates)
        self.sRateBox = QComboBox()
        self.sRateBox.addItem("44100")
        self.sRateBox.addItem("48000")
        self.sRateBox.setCurrentIndex(1)
        
        # add device and sr widgets to device layout
        deviceLayout.addWidget(self.deviceBox)
        deviceLayout.addWidget(sRateLabel)
        deviceLayout.addWidget(self.sRateBox)
        
        # add device layout to main layout
        layout.addLayout(deviceLayout)
        
        # DetectorBank parameters layout
        # two rows of three parameters
        # each param needs label and edit, 
        # and a 'Start' button will be added at the bottom
        # so grid should be 3x6
        detBankParamLayout = QGridLayout()
        
        # label and lineedit for each
        bandwidthLabel = QLabel("Bandwidth (cents):")
        dampingLabel = QLabel("Damping:")
        gainLabel = QLabel("Gain:")
        edoLabel = QLabel("EDO:")
        lwrLabel = QLabel("Lower note:")
        uprLabel = QLabel("Upper note:")
        
        self.bandwidthEdit = QLineEdit("0")
        self.dampingEdit = QLineEdit("0.0001")
        self.gainEdit = QLineEdit("25")
        self.edoEdit = QLineEdit("12")
        self.lwrEdit = QLineEdit("A1")
        self.uprEdit = QLineEdit("A7")
        
        # store all in lists
        detBankParamLabels = [bandwidthLabel, dampingLabel, gainLabel, 
                              edoLabel, lwrLabel, uprLabel]
        
        detBankParamEdits = [self.bandwidthEdit, self.dampingEdit, 
                             self.gainEdit, self.edoEdit, self.lwrEdit, 
                             self.uprEdit]
         
        # fill first two rows of grid with labels and edits
        row = 0

        for row in range(2):
            widgetNum = 0
        
            for i in range((row*3), (row*3)+3):
                detBankParamLayout.addWidget(detBankParamLabels[i], row, 
                                             widgetNum)
                widgetNum += 1
                detBankParamLayout.addWidget(detBankParamEdits[i], row, 
                                             widgetNum)
                widgetNum += 1
            
        # align labels to the right (next to the edit)
        for i in range(len(detBankParamLabels)):
            detBankParamLabels[i].setAlignment(Qt.AlignRight)
                
        # button to make DetectorBank and start visualisation
        row += 1
        startButton = QPushButton("&Start!")
        detBankParamLayout.addWidget(startButton, row, 5)
        startButton.clicked.connect(self.start)
        
        # add grid of detbank params (and start button) to main layout
        layout.addLayout(detBankParamLayout)
        
        window.setLayout(layout)
        self.setCentralWidget(window)
        self.show()
예제 #8
0
    def __init__(self):
        super().__init__()

        arguments = self.parseArguments()

        self.DEBUG = arguments.debug

        os.chdir(os.path.dirname(os.path.realpath(__file__)))

        self.MIN_WIDTH = 600
        self.MIN_HEIGHT = 350

        self.setMinimumWidth(self.MIN_WIDTH)
        self.setMinimumHeight(self.MIN_HEIGHT)

        self.ROOT_FOLDER = os.path.expanduser("~/.florodoro/")

        self.HISTORY_FILE_PATH = self.ROOT_FOLDER + "history" + ("" if not self.DEBUG else "-debug") + ".yaml"
        self.CONFIGURATION_FILE_PATH = self.ROOT_FOLDER + "config" + ("" if not self.DEBUG else "-debug") + ".yaml"

        self.history = History(self.HISTORY_FILE_PATH)

        self.SOUNDS_FOLDER = "sounds/"
        self.PLANTS_FOLDER = "plants/"
        self.IMAGE_FOLDER = "images/"

        self.TEXT_COLOR = self.palette().text().color()
        self.BREAK_COLOR = "#B37700"

        self.APP_NAME = "Florodoro"

        self.STUDY_ICON = qtawesome.icon('fa5s.book', color=self.TEXT_COLOR)
        self.BREAK_ICON = qtawesome.icon('fa5s.coffee', color=self.BREAK_COLOR)
        self.CONTINUE_ICON = qtawesome.icon('fa5s.play', color=self.TEXT_COLOR)
        self.PAUSE_ICON = qtawesome.icon('fa5s.pause', color=self.TEXT_COLOR)
        self.RESET_ICON = qtawesome.icon('fa5s.undo', color=self.TEXT_COLOR)

        self.PLANTS = [GreenTree, DoubleGreenTree, OrangeTree, CircularFlower]
        self.PLANT_NAMES = ["Spruce", "Double spruce", "Maple", "Flower"]

        self.WIDGET_SPACING = 10

        self.MAX_TIME = 180
        self.STEP = 5

        self.INITIAL_TEXT = "Start!"

        self.menuBar = QMenuBar(self)
        self.presets_menu = self.menuBar.addMenu('&Presets')

        self.presets = {
            "Classic": (25, 5, 4),
            "Extended": (45, 12, 2),
            "Sitcomodoro": (65, 25, 1),
        }

        for name in self.presets:
            study_time, break_time, cycles = self.presets[name]

            self.presets_menu.addAction(
                QAction(f"{name} ({study_time} : {break_time} : {cycles})", self,
                        triggered=partial(self.load_preset, study_time, break_time, cycles)))

        self.DEFAULT_PRESET = "Classic"

        self.options_menu = self.menuBar.addMenu('&Options')

        self.notify_menu = self.options_menu.addMenu("&Notify")

        self.sound_action = QAction("&Sound", self, checkable=True, checked=not self.DEBUG,
                                    triggered=lambda _: self.volume_slider.setDisabled(
                                        not self.sound_action.isChecked()))

        self.notify_menu.addAction(self.sound_action)

        # Default audio device
        self.audio_device = QAudioDeviceInfo.defaultInputDevice()

        # Create device menu
        self.audio_device_menu = self.notify_menu.addMenu("&Audio Devices")

        # For all sound devices, add a device check box to the device menu
        audio_devices = QAudioDeviceInfo.availableDevices(QAudio.AudioOutput)

        for device in audio_devices:
            device_name = device.deviceName()
            self.device_action = QAction(f"&{device_name}", self, checkable=True, checked=not self.DEBUG, triggered=partial(self.setAudioDevice, device))
            # Create callback of some sort for clicking on the device in the menu
            self.audio_device_menu.addAction(self.device_action)

        self.volume_slider = QSlider(Qt.Horizontal, minimum=0, maximum=100, value=85)
        slider_action = QWidgetAction(self)
        slider_action.setDefaultWidget(SpacedQWidget(self.volume_slider))
        self.notify_menu.addAction(slider_action)

        self.popup_action = QAction("&Pop-up", self, checkable=True, checked=True)
        self.notify_menu.addAction(self.popup_action)

        self.menuBar.addAction(
            QAction(
                "&Statistics",
                self,
                triggered=lambda: self.statistics.show() if self.statistics.isHidden() else self.statistics.hide()
            )
        )

        self.menuBar.addAction(
            QAction(
                "&About",
                self,
                triggered=lambda: QMessageBox.information(
                    self,
                    "About",
                    "This application was created by Tomáš Sláma. It is heavily inspired by the Android app Forest, "
                    "but with all of the plants generated procedurally. It's <a href='https://github.com/xiaoxiae/Florodoro'>open source</a> and licensed "
                    "under MIT, so do as you please with the code and anything else related to the project.",
                ),
            )
        )

        self.plant_menu = self.options_menu.addMenu("&Plants")

        self.overstudy_action = QAction("Overstudy", self, checkable=True)
        self.options_menu.addAction(self.overstudy_action)

        self.plant_images = []
        self.plant_checkboxes = []

        # dynamically create widgets for each plant
        for plant, name in zip(self.PLANTS, self.PLANT_NAMES):
            self.plant_images.append(tempfile.NamedTemporaryFile(suffix=".svg"))
            tmp = plant()
            tmp.set_max_age(1)
            tmp.set_age(1)
            tmp.save(self.plant_images[-1].name, 200, 200)

            setattr(self.__class__, name,
                    QAction(self, icon=QIcon(self.plant_images[-1].name), text=name, checkable=True, checked=True))

            action = getattr(self.__class__, name)

            self.plant_menu.addAction(action)
            self.plant_checkboxes.append(action)

        # the current plant that we're growing
        # if set to none, no plant is growing
        self.plant = None

        self.menuBar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum)

        main_vertical_layout = QVBoxLayout(self)
        main_vertical_layout.setContentsMargins(0, 0, 0, 0)
        main_vertical_layout.setSpacing(0)
        main_vertical_layout.addWidget(self.menuBar)

        self.canvas = Canvas(self)

        self.statistics = Statistics(self.history)

        font = self.font()
        font.setPointSize(100)

        self.main_label = QLabel(self, alignment=Qt.AlignCenter)
        self.main_label.setFont(font)
        self.main_label.setText(self.INITIAL_TEXT)

        font.setPointSize(26)
        self.cycle_label = QLabel(self)
        self.cycle_label.setAlignment(Qt.AlignTop)
        self.cycle_label.setMargin(20)
        self.cycle_label.setFont(font)

        main_horizontal_layout = QHBoxLayout(self)

        self.study_time_spinbox = QSpinBox(self, prefix="Study for: ", suffix="min.", minimum=1, maximum=self.MAX_TIME,
                                           singleStep=self.STEP)

        self.break_time_spinbox = QSpinBox(self, prefix="Break for: ", suffix="min.", minimum=1, maximum=self.MAX_TIME,
                                           singleStep=self.STEP,
                                           styleSheet=f'color:{self.BREAK_COLOR};')

        self.cycles_spinbox = QSpinBox(self, prefix="Cycles: ", minimum=1, value=1)

        # keep track of remaining number of cycles and the starting number of cycles
        self.remaining_cycles = 0
        self.total_cycles = 0

        # whether we're currently studying
        self.is_study_ongoing = False

        # whether we notified the user already during overstudy
        self.already_notified_during_overstudy = False

        stacked_layout = QStackedLayout(self, stackingMode=QStackedLayout.StackAll)
        stacked_layout.addWidget(self.main_label)
        stacked_layout.addWidget(self.cycle_label)
        stacked_layout.addWidget(self.canvas)

        main_vertical_layout.addLayout(stacked_layout)

        self.setStyleSheet("")

        self.study_button = QPushButton(self, clicked=self.start, icon=self.STUDY_ICON)
        self.break_button = QPushButton(self, clicked=self.start_break, icon=self.BREAK_ICON)
        self.pause_button = QPushButton(self, clicked=self.toggle_pause, icon=self.PAUSE_ICON)
        self.reset_button = QPushButton(self, clicked=self.reset, icon=self.RESET_ICON)

        main_horizontal_layout.addWidget(self.study_time_spinbox)
        main_horizontal_layout.addWidget(self.break_time_spinbox)
        main_horizontal_layout.addWidget(self.cycles_spinbox)
        main_horizontal_layout.addWidget(self.study_button)
        main_horizontal_layout.addWidget(self.break_button)
        main_horizontal_layout.addWidget(self.pause_button)
        main_horizontal_layout.addWidget(self.reset_button)

        main_vertical_layout.addLayout(main_horizontal_layout)

        self.setLayout(main_vertical_layout)

        self.study_timer_frequency = 1 / 60 * 1000
        self.study_timer = QTimer(self, interval=int(self.study_timer_frequency), timeout=self.decrease_remaining_time)

        self.player = QMediaPlayer(self)

        self.setWindowIcon(QIcon(self.IMAGE_FOLDER + "icon.svg"))
        self.setWindowTitle(self.APP_NAME)

        # set initial UI state
        self.reset()

        # a list of name, getter and setter things to load/save when the app opens/closes
        # also dynamically get settings for selecting/unselecting plants
        self.CONFIGURATION_ATTRIBUTES = [("study-time", self.study_time_spinbox.value,
                                          self.study_time_spinbox.setValue),
                                         ("break-time", self.break_time_spinbox.value,
                                          self.break_time_spinbox.setValue),
                                         ("cycles", self.cycles_spinbox.value, self.cycles_spinbox.setValue),
                                         ("sound", self.sound_action.isChecked, self.sound_action.setChecked),
                                         ("sound-volume", self.volume_slider.value, self.volume_slider.setValue),
                                         ("pop-ups", self.popup_action.isChecked, self.popup_action.setChecked),
                                         ("overstudy", self.overstudy_action.isChecked,
                                          self.overstudy_action.setChecked)] + \
                                        [(name.lower(), getattr(self.__class__, name).isChecked,
                                          getattr(self.__class__, name).setChecked) for _, name in
                                         zip(self.PLANTS, self.PLANT_NAMES)]
        # load the default preset
        self.load_preset(*self.presets[self.DEFAULT_PRESET])

        self.load_settings()
        self.show()
예제 #9
0
    def _uic(self):
        '''
    set up our layout which consists of:

                 Big Honkin' Label
        [input combobox]    [output combobox]
               [volume slider]  [x] Mute

    hooking put the signals to useful slots is the job
    of __init__. Here just make the layout.
        '''
        self.setMinimumWidth(400)
        # Create the big honkin' label and logo
        icon_pixmap = QPixmap(':/icon.png').scaledToWidth(64)
        icon_label = QLabel()
        icon_label.setPixmap(icon_pixmap)
        text_label = QLabel("Sidetone!")
        hb_label = QHBoxLayout()
        hb_label.addStretch(1)
        hb_label.addWidget(icon_label, 0)
        hb_label.addWidget(text_label, 0)
        hb_label.addStretch(1)

        # Create a list of QAudioInfo objects for inputs
        self.input_info_list = QAudioDeviceInfo.availableDevices(
            QAudio.AudioInput)
        if 0 == len(self.input_info_list):
            self.input_info_list = [QAudioDeviceInfo.defaultInputDevice()]
        # Create a combo box and populate it with names of inputs
        self.cb_inputs = QComboBox()
        self.cb_inputs.addItems(
            [audio_info.deviceName() for audio_info in self.input_info_list])
        # Create a list of QAudioInfo objects for outputs
        self.otput_info_list = QAudioDeviceInfo.availableDevices(
            QAudio.AudioOutput)
        if 0 == len(self.otput_info_list):
            self.otput_info_list = [QAudioDeviceInfo.defaultOutputDevice()]
        self.status_bar.showMessage(
            '{} inputs {} otputs'.format(len(self.input_info_list),
                                         len(self.otput_info_list)), 2000)
        # Create a combo box and populate it with names of outputs
        self.cb_otputs = QComboBox()
        self.cb_otputs.addItems(
            [audio_info.deviceName() for audio_info in self.otput_info_list])
        # Lay those two out aligned to the outside
        hb_combos = QHBoxLayout()
        hb_combos.addWidget(self.cb_inputs, 1)
        hb_combos.addStretch(0)
        hb_combos.addWidget(self.cb_otputs, 1)
        # Create a volume slider from 0 to 100.
        self.volume = QSlider(Qt.Horizontal, self)
        self.volume.setMinimum(0)
        self.volume.setMaximum(100)
        self.volume.setTickInterval(10)
        self.volume.setTickPosition(QSlider.TicksBothSides)
        # Create a checkbox "Mute"
        self.mute = QCheckBox('Mute')
        # Put those together in a row squeezed in the center
        hb_volume = QHBoxLayout()
        hb_volume.addStretch(1)
        hb_volume.addWidget(self.volume, 1)
        hb_volume.addWidget(self.mute, 0)
        hb_volume.addStretch(1)
        # Stack all those up as this widget's layout
        vlayout = QVBoxLayout()
        vlayout.addLayout(hb_label)
        vlayout.addLayout(hb_combos)
        vlayout.addLayout(hb_volume)
        self.setLayout(vlayout)
예제 #10
0
    def _uic(self):
        '''
    set up our layout which consists of:

                 Big Honkin' Label
        [input combobox]    [output combobox]
               [volume slider]  [x] Mute

    Hooking the signals to useful slots is the job
    of __init__. Here just make the layout.
        '''
        self.setMinimumWidth(400)
        # Create the big honkin' label and logo
        icon_pixmap = QPixmap(':/icon.png').scaledToWidth(64)
        icon_label = QLabel()
        icon_label.setPixmap(icon_pixmap)
        text_label = QLabel("Sidetone!")
        hb_label = QHBoxLayout()
        hb_label.addStretch(1)
        hb_label.addWidget(icon_label, 0)
        hb_label.addWidget(text_label, 0)
        hb_label.addStretch(1)

        # Create a list of QAudioInfo objects for inputs
        self.input_info_list = QAudioDeviceInfo.availableDevices(
            QAudio.AudioInput)
        if 0 == len(self.input_info_list):
            self.input_info_list = [QAudioDeviceInfo.defaultInputDevice()]
        # Make a list of the name-strings for those items.
        in_dev_names = [
            audio_info.deviceName() for audio_info in self.input_info_list
        ]
        # Create a combo box and populate it with those names
        self.cb_inputs = QComboBox()
        self.cb_inputs.addItems(in_dev_names)
        # If the in_dev_name from the previous run is in the current list,
        # make it current, otherwise pick the first item.
        in_dev_name = self.settings.value('in_dev_name', 'unknown')
        if in_dev_name in in_dev_names:
            self.cb_inputs.setCurrentIndex(in_dev_names.index(in_dev_name))
        else:
            self.cb_inputs.setCurrentIndex(0)

        # Create a list of QAudioInfo objects for outputs
        self.otput_info_list = QAudioDeviceInfo.availableDevices(
            QAudio.AudioOutput)
        if 0 == len(self.otput_info_list):
            self.otput_info_list = [QAudioDeviceInfo.defaultOutputDevice()]
        # Make a list of the name-strings of those things
        ot_dev_names = [
            audio_info.deviceName() for audio_info in self.otput_info_list
        ]
        # Create a combo box and populate it with those names
        self.cb_otputs = QComboBox()
        self.cb_otputs.addItems(ot_dev_names)
        # If the ot_dev_name from the previous run is in the current list,
        # make it the current choice in the box.
        ot_dev_name = self.settings.value('ot_dev_name', 'unknown')
        if ot_dev_name in ot_dev_names:
            self.cb_otputs.setCurrentIndex(ot_dev_names.index(ot_dev_name))
        else:
            self.cb_otputs.setCurrentIndex(0)

        #self.show_status(
        #'{} inputs {} otputs'.format(len(self.input_info_list),len(self.otput_info_list))
        #)
        # Create a combo box and populate it with names of outputs

        # Lay those two out aligned to the outside
        hb_combos = QHBoxLayout()
        hb_combos.addWidget(self.cb_inputs, 1)
        hb_combos.addStretch(0)
        hb_combos.addWidget(self.cb_otputs, 1)

        # Create a volume slider from 0 to 100.
        self.volume = QSlider(Qt.Horizontal, self)
        self.volume.setMinimum(0)
        self.volume.setMaximum(100)
        self.volume.setTickInterval(10)
        self.volume.setTickPosition(QSlider.TicksBothSides)
        # set the volume slider to the value from the previous run, or zero.
        self.volume.setValue(self.settings.value('volume', 0))

        # Create a checkbox "Mute"
        self.mute = QCheckBox('Mute')
        # Set it to the value at the end of the last run, or to True
        self.mute.setChecked(bool(self.settings.value('mute_status', 1)))

        # Put those together in a row squeezed in the center
        hb_volume = QHBoxLayout()
        hb_volume.addStretch(1)
        hb_volume.addWidget(self.volume, 1)
        hb_volume.addWidget(self.mute, 0)
        hb_volume.addStretch(1)

        # Stack all those up as this widget's layout
        vlayout = QVBoxLayout()
        vlayout.addLayout(hb_label)
        vlayout.addLayout(hb_combos)
        vlayout.addLayout(hb_volume)
        self.setLayout(vlayout)