class Generator_Widget(QtWidgets.QWidget): stream_stop_ramp_finished = QtCore.pyqtSignal() def __init__(self, parent): super().__init__(parent) self.audiobuffer = None self.setObjectName("Generator_Widget") self.grid_layout = QtWidgets.QGridLayout(self) self.grid_layout.setObjectName("grid_layout") self.generators = [] self.generators.append(SineGenerator(self)) self.generators.append(WhiteGenerator(self)) self.generators.append(PinkGenerator(self)) self.generators.append(SweepGenerator(self)) self.generators.append(BurstGenerator(self)) self.combobox_generator_kind = QtWidgets.QComboBox(self) self.combobox_generator_kind.setObjectName("combobox_generator_kind") self.stacked_settings_layout = QtWidgets.QStackedLayout() for generator in self.generators: self.combobox_generator_kind.addItem(generator.name) self.stacked_settings_layout.addWidget(generator.settingsWidget()) self.combobox_generator_kind.setCurrentIndex(DEFAULT_GENERATOR_KIND_INDEX) self.t = 0. self.t_start = 0. self.t_stop = RAMP_LENGTH self.state = STOPPED self.stream_stop_ramp_finished.connect(self.stop_stream_after_ramp) self.device = None self.stream = None # we will try to open all the output devices until one # works, starting by the default input device for device in AudioBackend().output_devices: Logger().push("Opening the stream for device: "+ device['name']) try: self.stream = AudioBackend().open_output_stream(device, self.audio_callback) self.stream.start() self.stream.stop() self.device = device Logger().push("Stream opened successfully") break except Exception as exception: Logger().push("Failed to open stream: " + str(exception)) self.start_stop_button = QtWidgets.QPushButton(self) startStopIcon = QtGui.QIcon() startStopIcon.addPixmap(QtGui.QPixmap(":/images-src/start.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off) startStopIcon.addPixmap(QtGui.QPixmap(":/images-src/stop.svg"), QtGui.QIcon.Normal, QtGui.QIcon.On) startStopIcon.addPixmap(QtGui.QPixmap(":/images-src/stop.svg"), QtGui.QIcon.Active, QtGui.QIcon.On) startStopIcon.addPixmap(QtGui.QPixmap(":/images-src/stop.svg"), QtGui.QIcon.Selected, QtGui.QIcon.On) startStopIcon.addPixmap(QtGui.QPixmap(":/images-src/stop.svg"), QtGui.QIcon.Disabled, QtGui.QIcon.On) self.start_stop_button.setIcon(startStopIcon) self.start_stop_button.setObjectName("generatorStartStop") self.start_stop_button.setText("Start") self.start_stop_button.setToolTip("Start/Stop generator") self.start_stop_button.setCheckable(True) self.start_stop_button.setChecked(False) self.grid_layout.addWidget(self.start_stop_button, 0, 0, 1, 1) self.grid_layout.addWidget(self.combobox_generator_kind, 1, 0, 1, 1) self.grid_layout.addLayout(self.stacked_settings_layout, 2, 0, 1, 1) self.combobox_generator_kind.activated.connect(self.stacked_settings_layout.setCurrentIndex) self.start_stop_button.toggled.connect(self.start_stop_button_toggle) # initialize the settings dialog devices = AudioBackend().get_readable_output_devices_list() if self.device is not None: device_index = AudioBackend().output_devices.index(self.device) else: device_index = None self.settings_dialog = Generator_Settings_Dialog(self, devices, device_index) self.settings_dialog.combobox_output_device.currentIndexChanged.connect(self.device_changed) # channels = AudioBackend().get_readable_current_output_channels() # for channel in channels: # self.settings_dialog.comboBox_firstChannel.addItem(channel) # self.settings_dialog.comboBox_secondChannel.addItem(channel) # current_device = AudioBackend().get_readable_current_output_device() # self.settings_dialog.combobox_output_device.setCurrentIndex(current_device) # first_channel = AudioBackend().get_current_first_channel() # self.settings_dialog.comboBox_firstChannel.setCurrentIndex(first_channel) # second_channel = AudioBackend().get_current_second_channel() # self.settings_dialog.comboBox_secondChannel.setCurrentIndex(second_channel) def device_changed(self, index): device = AudioBackend().output_devices[index] # save current stream in case we need to restore it previous_stream = self.stream previous_device = self.device error_message = "" Logger().push("Trying to write to output device " + device['name']) # first see if the format is supported by PortAudio try: success = AudioBackend().is_output_format_supported(device, np.int16) except Exception as exception: Logger().push("Format is not supported: " + str(exception)) success = False if success: try: self.stream = AudioBackend().open_output_stream(device, self.audio_callback) self.device = device self.stream.start() if self.state not in [STARTING, PLAYING]: self.stream.stop() success = True except OSError as error: Logger().push("Fail: " + str(error)) success = False if success: Logger().push("Success") previous_stream.stop() else: if self.stream is not None: self.stream.stop() # restore previous stream self.stream = previous_stream self.device = previous_device # 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 = QtWidgets.QErrorMessage(self.settings_dialog) error_message.setWindowTitle("Output device error") error_message.showMessage("Impossible to use the selected output device, reverting to the previous one. Reason is: " + error_message) self.settings_dialog.combobox_output_device.setCurrentIndex(AudioBackend().output_devices.index(self.device)) def settings_called(self, checked): self.settings_dialog.show() # method def set_buffer(self, buffer): self.audiobuffer = buffer # slot def start_stop_button_toggle(self, checked): if checked: self.start_stop_button.setText("Stop") if self.state == STOPPED or self.state == STOPPING: self.state = STARTING self.t_start = 0. self.stream.start() else: self.start_stop_button.setText("Start") if self.state == PLAYING or self.state == STARTING: self.state = STOPPING self.t_stop = RAMP_LENGTH # will stop at the end of the ramp def stop_stream_after_ramp(self): self.stream.stop() def handle_new_data(self, floatdata): # we do not make anything of the input data in the generator... return def audio_callback(self, out_data, frame_count, time_info, status): if status: print(status, flush=True) N = frame_count if self.state == STOPPED: out_data.fill(0) return # if we cannot write any sample, return now if N == 0: return t = self.t + np.arange(0, N / float(SAMPLING_RATE), 1. / float(SAMPLING_RATE)) name = self.combobox_generator_kind.currentText() generators = [generator for generator in self.generators if generator.name == name] if len(generators) == 0: print("generator error : index of signal type not found") out_data.fill(0) return if len(generators) > 1: print("generator error : 2 (or more) generators have the same name") out_data.fill(0) return generator = generators[0] floatdata = generator.signal(t) # add smooth ramps at start/stop to avoid undesirable bursts if self.state == STARTING: # add a ramp at the start t_ramp = self.t_start + np.arange(0, N / float(SAMPLING_RATE), 1. / float(SAMPLING_RATE)) t_ramp = np.clip(t_ramp, 0., RAMP_LENGTH) floatdata *= t_ramp / RAMP_LENGTH self.t_start += N / float(SAMPLING_RATE) if self.t_start > RAMP_LENGTH: self.state = PLAYING if self.state == STOPPING: print("stopping", self.t_stop, N) # add a ramp at the end t_ramp = self.t_stop - np.arange(0, N / float(SAMPLING_RATE), 1. / float(SAMPLING_RATE)) t_ramp = np.clip(t_ramp, 0., RAMP_LENGTH) floatdata *= t_ramp / RAMP_LENGTH self.t_stop -= N / float(SAMPLING_RATE) if self.t_stop < 0.: self.state = STOPPED self.stream_stop_ramp_finished.emit() # output channels are interleaved # we output to all channels simultaneously with the same data maxOutputChannels = AudioBackend().get_device_outputchannels_count(self.device) floatdata = np.tile(floatdata, (maxOutputChannels, 1)).transpose() int16info = np.iinfo(np.int16) norm_coeff = min(abs(int16info.min), int16info.max) intdata = (np.clip(floatdata, int16info.min, int16info.max) * norm_coeff).astype(np.int16) # update the time counter self.t += N / float(SAMPLING_RATE) # data copy out_data[:] = intdata def canvasUpdate(self): return def saveState(self, settings): settings.setValue("generator kind", self.combobox_generator_kind.currentIndex()) for generator in self.generators: generator.settingsWidget().saveState(settings) self.settings_dialog.saveState(settings) def restoreState(self, settings): generator_kind = settings.value("generator kind", DEFAULT_GENERATOR_KIND_INDEX, type=int) self.combobox_generator_kind.setCurrentIndex(generator_kind) self.stacked_settings_layout.setCurrentIndex(generator_kind) for generator in self.generators: generator.settingsWidget().restoreState(settings) self.settings_dialog.restoreState(settings)