Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add export features #437

Merged
merged 5 commits into from
May 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/urh/controller/widgets/SignalFrame.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ def create_connects(self):
self.ui.sliderSpectrogramMax.valueChanged.connect(self.on_slider_spectrogram_max_value_changed)
self.ui.gvSpectrogram.y_scale_changed.connect(self.on_gv_spectrogram_y_scale_changed)
self.ui.gvSpectrogram.bandpass_filter_triggered.connect(self.on_bandpass_filter_triggered)
self.ui.gvSpectrogram.export_fta_wanted.connect(self.on_export_fta_wanted)
self.ui.btnAdvancedModulationSettings.clicked.connect(self.on_btn_advanced_modulation_settings_clicked)

if self.signal is not None:
Expand Down Expand Up @@ -191,6 +192,8 @@ def create_connects(self):

self.ui.gvSignal.set_noise_clicked.connect(self.on_set_noise_in_graphic_view_clicked)
self.ui.gvSignal.save_as_clicked.connect(self.save_signal_as)
self.ui.gvSignal.export_demodulated_clicked.connect(self.export_demodulated)

self.ui.gvSignal.create_clicked.connect(self.create_new_signal)
self.ui.gvSignal.zoomed.connect(self.on_signal_zoomed)
self.ui.gvSpectrogram.zoomed.connect(self.on_spectrum_zoomed)
Expand Down Expand Up @@ -409,6 +412,27 @@ def save_signal_as(self):
except Exception as e:
QMessageBox.critical(self, self.tr("Error saving signal"), e.args[0])

def export_demodulated(self):
try:
initial_name = self.signal.name + "-demodulated.complex"
except Exception as e:
logger.exception(e)
initial_name = "demodulated.complex"

filename = FileOperator.get_save_file_name(initial_name)
if filename:
try:
self.setCursor(Qt.WaitCursor)
data = self.signal.qad
if filename.endswith(".wav"):
data = self.signal.qad.astype(np.float32)
data /= np.max(np.abs(data))
data = FileOperator.convert_data_to_format(data, filename)
FileOperator.save_data(data, filename, self.signal.sample_rate, num_channels=1)
self.unsetCursor()
except Exception as e:
QMessageBox.critical(self, self.tr("Error exporting demodulated data"), e.args[0])

def draw_signal(self, full_signal=False):
gv_legend = self.ui.gvLegend
gv_legend.y_sep = -self.signal.qad_center
Expand Down Expand Up @@ -1210,3 +1234,24 @@ def on_btn_advanced_modulation_settings_clicked(self):
dialog.message_length_divisor_edited.connect(self.on_message_length_divisor_edited)
dialog.exec_()

@pyqtSlot()
def on_export_fta_wanted(self):
try:
initial_name = self.signal.name + "-spectrogram.ft"
except Exception as e:
logger.exception(e)
initial_name = "spectrogram.ft"

filename = FileOperator.get_save_file_name(initial_name, caption="Export spectrogram")
if not filename:
return
QApplication.setOverrideCursor(Qt.WaitCursor)
try:
self.ui.gvSpectrogram.scene_manager.spectrogram.export_to_fta(sample_rate=self.signal.sample_rate,
filename=filename,
include_amplitude=filename.endswith(".fta"))
except Exception as e:
logger.exception(e)
Errors.generic_error("Failed to export spectrogram", str(e))
finally:
QApplication.restoreOverrideCursor()
28 changes: 28 additions & 0 deletions src/urh/signalprocessing/Spectrogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,34 @@ def stft(self, samples: np.ndarray):
result = np.fft.fft(frames * window, self.window_size) / np.atleast_1d(self.window_size)
return result

def export_to_fta(self, sample_rate, filename: str, include_amplitude=False):
"""
Export to Frequency, Time, Amplitude file.
Frequency is double, Time (nanosecond) is uint32, Amplitude is float32

:return:
"""
spectrogram = self.__calculate_spectrogram(self.samples)
spectrogram = np.flipud(spectrogram.T)
if include_amplitude:
result = np.empty((spectrogram.shape[0], spectrogram.shape[1], 3),
dtype=[('f', np.float64), ('t', np.uint32), ('a', np.float32)])
else:
result = np.empty((spectrogram.shape[0], spectrogram.shape[1], 2),
dtype=[('f', np.float64), ('t', np.uint32)])

fft_freqs = np.fft.fftshift(np.fft.fftfreq(spectrogram.shape[0], 1/sample_rate))
time_width = 1e9 * ((len(self.samples) / sample_rate) / spectrogram.shape[1])

for i in range(spectrogram.shape[0]):
for j in range(spectrogram.shape[1]):
if include_amplitude:
result[i, j] = (fft_freqs[i], int(j*time_width), spectrogram[i, j])
else:
result[i, j] = (fft_freqs[i], int(j * time_width))

result.tofile(filename)

def __calculate_spectrogram(self, samples: np.ndarray) -> np.ndarray:
# Only shift axis 1 (frequency) and not time
spectrogram = np.fft.fftshift(self.stft(samples), axes=(1,))
Expand Down
18 changes: 12 additions & 6 deletions src/urh/ui/views/EditableGraphicView.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

class EditableGraphicView(ZoomableGraphicView):
save_as_clicked = pyqtSignal()
export_demodulated_clicked = pyqtSignal()
create_clicked = pyqtSignal(int, int)
set_noise_clicked = pyqtSignal()
participant_changed = pyqtSignal()
Expand Down Expand Up @@ -130,12 +131,6 @@ def create_context_menu(self):
self.paste_position = int(self.mapToScene(self.context_menu_position).x())

menu = QMenu(self)
if self.save_enabled:
menu.addAction(self.save_action)

menu.addAction(self.save_as_action)
menu.addSeparator()

menu.addAction(self.copy_action)
self.copy_action.setEnabled(self.something_is_selected)
menu.addAction(self.paste_action)
Expand Down Expand Up @@ -203,6 +198,17 @@ def create_context_menu(self):
menu.addAction(self.undo_action)
menu.addAction(self.redo_action)

if self.scene_type == 0:
menu.addSeparator()
if self.save_enabled:
menu.addAction(self.save_action)

menu.addAction(self.save_as_action)
elif self.scene_type == 1:
menu.addSeparator()
export_demod_action = menu.addAction("Export demodulated...")
export_demod_action.triggered.connect(self.export_demodulated_clicked.emit)

return menu

def clear_horizontal_selection(self):
Expand Down
13 changes: 13 additions & 0 deletions src/urh/ui/views/SpectrogramGraphicView.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class SpectrogramGraphicView(ZoomableGraphicView):
MINIMUM_VIEW_WIDTH = 10
y_scale_changed = pyqtSignal(float)
bandpass_filter_triggered = pyqtSignal(float, float)
export_fta_wanted = pyqtSignal()

def __init__(self, parent=None):
super().__init__(parent)
Expand Down Expand Up @@ -68,6 +69,11 @@ def create_context_menu(self):
configure_filter_bw.triggered.connect(self.on_configure_filter_bw_triggered)
configure_filter_bw.setIcon(QIcon.fromTheme("configure"))

menu.addSeparator()

export_fta_action = menu.addAction("Export spectrogram...")
export_fta_action.triggered.connect(self.on_export_fta_action_triggered)

return menu

def zoom_to_selection(self, start: int, end: int):
Expand Down Expand Up @@ -101,3 +107,10 @@ def __get_freqs(self):
def on_configure_filter_bw_triggered(self):
dialog = FilterBandwidthDialog(parent=self)
dialog.show()

@pyqtSlot()
def on_export_fta_action_triggered(self):
if not(self.scene_manager and self.scene_manager.spectrogram):
return

self.export_fta_wanted.emit()
22 changes: 13 additions & 9 deletions src/urh/util/FileOperator.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ def get_save_file_name(initial_name: str, wav_only=False, caption="Save signal")
name_filter = ""
elif caption == "Save simulator profile":
name_filter = "Simulator (*.sim.xml *.sim);;All files (*)"
elif caption == "Export spectrogram":
name_filter = "Frequency Time (*.ft);;Frequency Time Amplitude (*.fta)"
else:
name_filter = "Protocols (*.proto.xml *.proto);;All files (*)"

Expand Down Expand Up @@ -146,10 +148,10 @@ def save_data_dialog(signal_name: str, data, wav_only=False, parent=None) -> str
return filename


def save_data(data, filename: str, sample_rate=1e6):
def save_data(data, filename: str, sample_rate=1e6, num_channels=2):
if filename.endswith(".wav"):
f = wave.open(filename, "w")
f.setnchannels(2)
f.setnchannels(num_channels)
f.setsampwidth(2)
f.setframerate(sample_rate)
f.writeframes(data)
Expand All @@ -174,18 +176,20 @@ def save_data(data, filename: str, sample_rate=1e6):
rewrite_tar(archive)


def save_signal(signal):
filename = signal.filename
def convert_data_to_format(data: np.ndarray, filename: str):
if filename.endswith(".wav"):
data = signal.wave_data
return (data.view(np.float32) * 32767).astype(np.int16)
elif filename.endswith(".complex16u"):
data = (127.5 * (signal.data.view(np.float32) + 1.0)).astype(np.uint8)
return (127.5 * (data.view(np.float32) + 1.0)).astype(np.uint8)
elif filename.endswith(".complex16s"):
data = (127.5 * ((signal.data.view(np.float32)) - 0.5 / 127.5)).astype(np.int8)
return (127.5 * ((data.view(np.float32)) - 0.5 / 127.5)).astype(np.int8)
else:
data = signal.data
return data


save_data(data, filename, sample_rate=signal.sample_rate)
def save_signal(signal):
data = convert_data_to_format(signal.data, signal.filename)
save_data(data, signal.filename, sample_rate=signal.sample_rate)


def rewrite_zip(zip_name):
Expand Down
3 changes: 3 additions & 0 deletions tests/.coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ exclude_lines =
class SectionComboBox
def createEditor
def updateEditorGeometry
def on_export_fta_wanted
def export_to_fta
def export_demodulated
15 changes: 15 additions & 0 deletions tests/test_signal_tab_GUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from tests.QtTestCase import QtTestCase
from tests.utils_testing import get_path_for_data_file
from urh.controller.MainController import MainController
from urh.signalprocessing.Participant import Participant


Expand Down Expand Up @@ -266,3 +267,17 @@ def test_context_menu_text_edit_protocol_view(self):
text_edit.selectAll()
menu = text_edit.create_context_menu()
self.assertEqual(len([action for action in menu.actions() if action.text() == "Participant"]), 1)

def test_export_demodulated(self):
self.add_signal_to_form("esaver.complex")
assert isinstance(self.form, MainController)
self.form.signal_tab_controller.signal_frames[0].ui.gvSignal.context_menu_position = QPoint(0,0)
cm = self.form.signal_tab_controller.signal_frames[0].ui.gvSignal.create_context_menu()
export_action = next((a for a in cm.actions() if "demodulated" in a.text().lower()), None)
self.assertIsNone(export_action)

self.form.signal_tab_controller.signal_frames[0].ui.cbSignalView.setCurrentIndex(1)
cm = self.form.signal_tab_controller.signal_frames[0].ui.gvSignal.create_context_menu()
export_action = next((a for a in cm.actions() if "demodulated" in a.text().lower()), None)
self.assertIsNotNone(export_action)

2 changes: 1 addition & 1 deletion tests/test_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ def test_external_program_simulator(self):
bits = self.__demodulate(conn)
self.assertEqual(bits[0], "101010101")

QTest.qWait(250)
QTest.qWait(500)
self.assertTrue(simulator.simulation_is_finished())

conn.close()
Expand Down