Пример #1
0
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self._ui = Ui_MainWindow()
        self._ui.setupUi(self)
        self.device = None
        self.analyzer = None
        self.in_error = False
        self.current_expected_pcm_clock_rate = None
        self.error_ticks = 0
        self.error_counts = 0

        self.audio = pyaudio.PyAudio()
        self.event_listener = SaleaeEventListener()
        self.event_listener.saleae_event_received.connect(self.on_saleae_event)
        self.play_sound_thread = SoundOutputRenderer(self.audio)
        self.analyzed_data_received_event.connect(self.play_sound_thread.on_data_received)
        PyDevicesManager.register_listener(self.event_listener, EVENT_ID_ONCONNECT)
        PyDevicesManager.register_listener(self.event_listener, EVENT_ID_ONDISCONNECT)
        PyDevicesManager.register_listener(self.event_listener, EVENT_ID_ONERROR)
        PyDevicesManager.register_listener(self.event_listener, EVENT_ID_ONREADDATA)
        PyDevicesManager.register_listener(self.event_listener, EVENT_ID_ONANALYZERDATA)

        self.audio_output_devices = []
        for i in range(self.audio.get_device_count()):
            info = self.audio.get_device_info_by_index(i)
            if info['maxOutputChannels'] > 0:
                self.audio_output_devices.append(info)
        self.initialize_ui_items()

        self.recording_state = STATE_IDLE
        self.last_record_start = time.clock()
        self.realtime_timer = QtCore.QTimer()
        self.realtime_timer.timeout.connect(self.realtime_timer_timeout)
        self.plot_timer = QtCore.QTimer()
        self.plot_timer.timeout.connect(self.plot_timer_timeout)

        self.figure = Figure(dpi=100)
        self.plotCanvas = FigureCanvas(self.figure)
        self.plotCanvas.setParent(self._ui.plotWidget)
        # Hook this up so we can resize the plot canvas dynamically
        self._ui.plotWidget.installEventFilter(self)
        self.fft_axis = self.figure.add_subplot(111)
        self.fft_line = None
        ytick_values = range(-140, -6, 6)
        self.fft_axis.set_yticks(ytick_values)
        self.fft_axis.set_yticklabels(["%d" % w for w in ytick_values], size='xx-small')
        self.fft_axis.set_xlabel("Frequency (kHz)", size='small')
        self.fft_axis.set_ylabel("dBFS", size='small')
        self.fft_axis.grid(True)
        self.fft_axis.autoscale(enable=False, axis='both')
        self.plot_background = None

        self.update_controls()

        self.show_message("Waiting for a Logic device to connect...")
        self.event_listener.start()
        PyDevicesManager.begin_connect()
Пример #2
0
class MainWindow(QMainWindow):
    analyzed_data_received_event = QtCore.Signal(object)

    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self._ui = Ui_MainWindow()
        self._ui.setupUi(self)
        self.device = None
        self.analyzer = None
        self.in_error = False
        self.current_expected_pcm_clock_rate = None
        self.error_ticks = 0
        self.error_counts = 0

        self.audio = pyaudio.PyAudio()
        self.event_listener = SaleaeEventListener()
        self.event_listener.saleae_event_received.connect(self.on_saleae_event)
        self.play_sound_thread = SoundOutputRenderer(self.audio)
        self.analyzed_data_received_event.connect(self.play_sound_thread.on_data_received)
        PyDevicesManager.register_listener(self.event_listener, EVENT_ID_ONCONNECT)
        PyDevicesManager.register_listener(self.event_listener, EVENT_ID_ONDISCONNECT)
        PyDevicesManager.register_listener(self.event_listener, EVENT_ID_ONERROR)
        PyDevicesManager.register_listener(self.event_listener, EVENT_ID_ONREADDATA)
        PyDevicesManager.register_listener(self.event_listener, EVENT_ID_ONANALYZERDATA)

        self.audio_output_devices = []
        for i in range(self.audio.get_device_count()):
            info = self.audio.get_device_info_by_index(i)
            if info['maxOutputChannels'] > 0:
                self.audio_output_devices.append(info)
        self.initialize_ui_items()

        self.recording_state = STATE_IDLE
        self.last_record_start = time.clock()
        self.realtime_timer = QtCore.QTimer()
        self.realtime_timer.timeout.connect(self.realtime_timer_timeout)
        self.plot_timer = QtCore.QTimer()
        self.plot_timer.timeout.connect(self.plot_timer_timeout)

        self.figure = Figure(dpi=100)
        self.plotCanvas = FigureCanvas(self.figure)
        self.plotCanvas.setParent(self._ui.plotWidget)
        # Hook this up so we can resize the plot canvas dynamically
        self._ui.plotWidget.installEventFilter(self)
        self.fft_axis = self.figure.add_subplot(111)
        self.fft_line = None
        ytick_values = range(-140, -6, 6)
        self.fft_axis.set_yticks(ytick_values)
        self.fft_axis.set_yticklabels(["%d" % w for w in ytick_values], size='xx-small')
        self.fft_axis.set_xlabel("Frequency (kHz)", size='small')
        self.fft_axis.set_ylabel("dBFS", size='small')
        self.fft_axis.grid(True)
        self.fft_axis.autoscale(enable=False, axis='both')
        self.plot_background = None

        self.update_controls()

        self.show_message("Waiting for a Logic device to connect...")
        self.event_listener.start()
        PyDevicesManager.begin_connect()

    def initialize_ui_items(self,):
        self._ui.onDecodeErrorComboBox.addItems(ON_DECODE_ERROR_OPTS)
        self._ui.onDecodeErrorComboBox.setCurrentIndex(HALT)
        self._ui.frameAlignmentComboBox.addItems(FRAME_ALIGNMENTS)
        self._ui.frameAlignmentComboBox.setCurrentIndex(FRAME_ALIGN_LAST_BIT)
        self._ui.clockEdgeComboBox.addItems(EDGES)
        self._ui.clockEdgeComboBox.setCurrentIndex(FALLING_EDGE)
        self._ui.outputLocationLineEdit.setText(RUN_PATH)

        for item in self.audio_output_devices:
            self._ui.comboOutputDeviceSelection.addItem(item['name'], item)
        # Select the default audio output
        default = self.audio.get_default_output_device_info()
        index = self._ui.comboOutputDeviceSelection.findData(default)
        if index < 0:
            index = 0
        self._ui.comboOutputDeviceSelection.setCurrentIndex(index)

        num_channels = self._ui.channelsPerFrameSpinBox.value()
        self._ui.comboPCMChannelToListenTo.addItems(['%d' % w for w in range(1, num_channels + 1)])
        self._ui.comboPCMChannelToListenTo.setCurrentIndex(0)

    def show_message(self, msg):
        self._ui.messagesLabel.setText(msg)

    def start_recording(self, mode):
        # Create an analyzer
        channels_per_frame = self._ui.channelsPerFrameSpinBox.value()
        sampling_rate = int(self._ui.samplingRateLineEdit.text())
        bits_per_channel = self._ui.bitsPerChannelSpinBox.value()
        clock_channel = self._ui.clockChannelSpinBox.value()
        frame_channel = self._ui.frameChannelSpinBox.value()
        data_channel = self._ui.dataChannelSpinBox.value()
        clock_edge = self._ui.clockEdgeComboBox.currentIndex()
        frame_edge = LEADING_EDGE
        self.current_expected_pcm_clock_rate = \
                    channels_per_frame * sampling_rate * bits_per_channel

        if clock_edge == LEADING_EDGE:
            frame_edge = FALLING_EDGE
        decode_error = self._ui.onDecodeErrorComboBox.currentIndex()
        frame_transition = self._ui.frameAlignmentComboBox.currentIndex()
        output_dir = None
        if mode == MODE_WRITE_TO_FILE:
            output_dir = self._ui.outputLocationLineEdit.text()
        plot_spectrum = self._ui.checkboxShowSpectrum.isChecked()
        self.analyzer = PCMAnalyzer(
                               output_folder = output_dir,
                               audio_channels_per_frame = channels_per_frame,
                               audio_sampling_rate_hz = sampling_rate,
                               bits_per_channel = bits_per_channel,
                               clock_channel = clock_channel,
                               frame_channel = frame_channel,
                               data_channel = data_channel,
                               frame_align = frame_edge,
                               frame_transition = frame_transition,
                               clock_edge = clock_edge,
                               on_decode_error = decode_error,
                               calculate_ffts = plot_spectrum,
                               logging = False)     # Do not enable this unless you have a HUUUGE hard drive!
        self.device.set_analyzer(self.analyzer)
        self.device.set_active_channels(list(range(4)))
        rate = self.device.get_analyzer().get_minimum_acquisition_rate()
        self.device.set_sampling_rate_hz(rate)
        self.device.set_use_5_volts(False)
        self.recording_state = STATE_WAITING_FOR_FRAME

        self.last_record_start = time.clock()
        self.realtime_timer.start(100)
        if plot_spectrum:
            self.plot_timer.start(150)

        if mode == MODE_LISTEN:
            # Configure the audio player
            data = self._ui.comboOutputDeviceSelection.itemData(
                        self._ui.comboOutputDeviceSelection.currentIndex())
            format = pyaudio.paInt16
            if bits_per_channel > 16:
                format = pyaudio.paInt32
            self.play_sound_thread.configure(data['index'], format=format,
                    channels=1, rate = sampling_rate,
                    channel_to_play=self._ui.comboPCMChannelToListenTo.currentIndex())
            self.play_sound_thread.start()

        self.show_message("Waiting for valid frame...")
        self.device.read_start()
        self.update_controls()

    def stop_recording(self,):
        self.reset()
        self.recording_state = STATE_IDLE
        self.show_message("Recording stopped.")
        self.update_controls()
        self.validate()

    def eventFilter(self, object, event):
        if event.type() == QtCore.QEvent.Resize:
            if object == self._ui.plotWidget:
                self.update_plot_canvas()
        return QWidget.eventFilter(self, object, event)

    @QtCore.Slot()
    def on_recordButton_clicked(self,):
        if self._ui.recordButton.text() == 'Record':
            self.start_recording(MODE_WRITE_TO_FILE)
        elif self._ui.recordButton.text() == 'Listen':
            self.start_recording(MODE_LISTEN)
        else:
            self.stop_recording()

    def realtime_timer_timeout(self,):
        elapsed = time.clock() - self.last_record_start
        time_text = "%d:%02.1f" % (elapsed / 60, elapsed % 60)
        self._ui.recordTimeLabel.setText(time_text)
        if self.in_error:
            if self.error_ticks > 15:
                self._ui.messagesLabel.setStyleSheet("background-color: none;")
                self.show_message("Continuing recording (last error: %s)..." % time_text)
                self.error_ticks = 0
                self.error_counts = 0
                self.in_error = False
            else:
                self._ui.messagesLabel.setStyleSheet("background-color: red;")
                self.error_ticks += 1

    def plot_timer_timeout(self, d=None):
        if self.device is not None and self.device.get_analyzer() is not None:
            analyzer = self.device.get_analyzer()
            data = analyzer.get_latest_fft_data(purge=True)
            if data is not None:
                self.update_plot(data)

    @QtCore.Slot()
    def on_outputLocationBrowseButton_clicked(self,):
        # Prompt for file name
        dirname = QFileDialog.getExistingDirectory(self, "Select Output Folder",
                            self._ui.outputLocationLineEdit.text(),
                            QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks)
        # Returns a tuple of (filename, filetype)
        if dirname is not None and len(dirname) > 0:
            self._ui.outputLocationLineEdit.setText(dirname)

    @QtCore.Slot(int)
    def on_channelsPerFrameSpinBox_valueChanged(self, i):
        self._ui.comboPCMChannelToListenTo.clear()
        num_channels = self._ui.channelsPerFrameSpinBox.value()
        self._ui.comboPCMChannelToListenTo.addItems(['%d' % w for w in range(1, num_channels + 1)])
        self._ui.comboPCMChannelToListenTo.setCurrentIndex(0)
        self.validate()

    @QtCore.Slot(int)
    def on_clockChannelSpinBox_valueChanged(self, i):
        self.validate()

    @QtCore.Slot(int)
    def on_frameChannelSpinBox_valueChanged(self, i):
        self.validate()

    @QtCore.Slot(int)
    def on_dataChannelSpinBox_valueChanged(self, i):
        self.validate()

    @QtCore.Slot(int)
    def on_comboPCMChannelToListenTo_currentIndexChanged(self, i):
        if self.play_sound_thread is not None:
            self.play_sound_thread.channel_to_play = self._ui.comboPCMChannelToListenTo.currentIndex()

    @QtCore.Slot(int)
    def on_comboOutputDeviceSelection_currentIndexChanged(self, i):
        pass

    @QtCore.Slot()
    def on_tabOutputRenderer_currentChanged(self,):
        if self.recording_state != STATE_IDLE:
            self.stop_recording()
        self.update_controls()

    def update_controls(self,):
        is_recording = (self.recording_state != STATE_IDLE)
        if not is_recording:
            current_tab = self._ui.tabOutputRenderer.currentWidget()
            if current_tab == self._ui.recordToFile:
                self._ui.recordButton.setText("Record")
            elif current_tab == self._ui.outputToSoundCard:
                self._ui.recordButton.setText("Listen")
        else:
            self._ui.recordButton.setText("Stop")

        self._ui.comboOutputDeviceSelection.setEnabled(not is_recording)
        self._ui.outputLocationBrowseButton.setEnabled(not is_recording)
        self._ui.logicGroupBox.setEnabled(not is_recording)
        self._ui.pcmGroupBox.setEnabled(not is_recording)
        self._ui.outputGroupBox.setEnabled(not is_recording)
        self._ui.checkboxShowSpectrum.setEnabled(not is_recording)
        self.update_plot_canvas()

    def update_plot_canvas(self,):
        self.fft_axis.set_xlim(0, int(self._ui.samplingRateLineEdit.text()) / 2)
        freq_values = range(0, int(self._ui.samplingRateLineEdit.text()) / 2, 1000) + \
                            [int(self._ui.samplingRateLineEdit.text()) / 2]
        self.fft_axis.set_xticks(freq_values)
        self.fft_axis.set_xticklabels(["%d" % (w / 1000) for w in freq_values], size='xx-small')
        self.plotCanvas.resize(self._ui.plotWidget.size().width(),
                               self._ui.plotWidget.size().height())
        self.plotCanvas.draw()
        self.plot_background = None

    def validate(self,):
        valid = False
        if self.device is not None:
            if (self._ui.clockChannelSpinBox.value() != self._ui.frameChannelSpinBox.value()) and \
               (self._ui.frameChannelSpinBox.value() != self._ui.dataChannelSpinBox.value()) and \
               (self._ui.clockChannelSpinBox.value() != self._ui.dataChannelSpinBox.value()):
                dirname = self._ui.outputLocationLineEdit.text()
                if dirname is not None and len(dirname) > 0:
                    valid = True
        self.set_valid(valid)

    def set_valid(self, is_valid):
        self._ui.recordButton.setEnabled(is_valid)

    def on_saleae_event(self, event, device):
        analyzer = None
        if device is not None and device == self.device and \
                self.device.get_analyzer() is not None:
            analyzer = self.device.get_analyzer()
        if event.id == EVENT_ID_ONCONNECT:
            self.show_message("Device connected with id %d" % device.get_id())
            self.device = device
            self.update_controls()
            self.validate()
        elif event.id == EVENT_ID_ONERROR:
            if self._ui.onDecodeErrorComboBox.currentIndex() == HALT:
                self.stop_recording()
                self.show_message("ERROR: %s" % event.data)
            else:
                if not self.in_error:
                    self.in_error = True
                    self.show_message("ERROR: %s" % event.data)
                else:
                    self.error_counts += 1
                    if self.error_counts > 5:
                        self.stop_recording()
                        self.show_message("Too many errors! %s" % event.data)
        elif event.id == EVENT_ID_ONDISCONNECT:
            self.recording_state = STATE_IDLE
            self.show_message("Device id %d disconnected." % device.get_id())
            self.shutdown()
        elif event.id == EVENT_ID_ONREADDATA:
            if analyzer is not None:
                if self.recording_state == STATE_WAITING_FOR_FRAME:
                    if self.device.get_analyzer().first_valid_frame_received():
                        self.show_message("Recording. Press 'Stop' to stop recording.")
                        self.recording_state = STATE_RECORDING
        elif event.id == EVENT_ID_ONANALYZERDATA:
            if self.recording_state == STATE_RECORDING and \
                    self.current_expected_pcm_clock_rate is not None:
                # Sanity check the sampling rate with the detected clock frequency
                if analyzer is not None:
                    clock_period_samples = self.device.get_analyzer().get_average_clock_period_in_samples()
                    meas_clock_freq = self.device.get_sampling_rate_hz() / clock_period_samples
                    if (1.2 * self.current_expected_pcm_clock_rate) <  meas_clock_freq or \
                        (0.8 * self.current_expected_pcm_clock_rate) > meas_clock_freq:
                        # The user's setup is probably wrong, so bail immediately
                        self.stop_recording()
                        self.show_message("Detected a PCM clock of ~%d Hz. Check your settings!" % meas_clock_freq)

            self.analyzed_data_received_event.emit(event.data)

    def update_plot(self, data):
        if self.plot_background is None:
            self.plot_background = self.plotCanvas.copy_from_bbox(self.fft_axis.bbox)

        channel = self._ui.comboPCMChannelToListenTo.currentIndex()
        channel_data = data[channel]
        numpoints = len(channel_data)
        if self.fft_line is None:
            self.fft_line, = self.fft_axis.plot(numpy.zeros(numpoints), animated=True)

        sampling_rate = int(self._ui.samplingRateLineEdit.text())
        freqs = numpy.fft.fftfreq(numpoints * 2, d=1.0 / float(sampling_rate))

        # Restore the clean slate background (this is the 'blit' method, which
        # is much faster to render)
        self.plotCanvas.restore_region(self.plot_background, bbox=self.fft_axis.bbox)

        self.fft_line.set_ydata(channel_data)
        self.fft_line.set_xdata(freqs[:numpoints])
        # Draw the line
        self.fft_axis.draw_artist(self.fft_line)
        # Blit the canvas
        self.plotCanvas.blit(self.fft_axis.bbox)

    def reset(self,):
        self.current_expected_pcm_clock_rate = None
        if self.device is not None:
            self.device.stop()
            self.analyzer = None
        self.realtime_timer.stop()
        self.plot_timer.stop()
        if self.play_sound_thread.isRunning():
            self.play_sound_thread.quit()
            self.play_sound_thread.wait()
            
        self._ui.messagesLabel.setStyleSheet("background-color: none;")
        self.error_ticks = 0
        self.error_counts = 0
        self.in_error = False

    def shutdown(self,):
        self.recording_state = STATE_IDLE
        self.reset()
        self.device = None
        try:
            self.figure.close()
        except:
            pass
    
    def closeEvent(self, event):
        """Intercepts the close event of the MainWindow."""
        self.show_message("Closing device...")
        try:
            self.shutdown()
            self.event_listener.quit()
            self.event_listener.wait()
            self.audio.terminate()
        finally:
            super(MainWindow, self).closeEvent(event)