diff --git a/ZXTapeReviver.pro b/ZXTapeReviver.pro
index bdcffcb..d78c214 100644
--- a/ZXTapeReviver.pro
+++ b/ZXTapeReviver.pro
@@ -11,7 +11,7 @@
# permission of the Author.
#*******************************************************************************
-QT += quick gui quickcontrols2
+QT += quick gui quickcontrols2 multimedia
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
@@ -78,26 +78,38 @@ DEFINES += TRANSLATION_IDS_HEADER="\\\"$${TRANSLATIONS_GENERATED_FILENAME_H}\\\"
ZXTAPEREVIVER_VERSION=\\\"$${ZXTAPEREVIVER_VERSION}\\\"
SOURCES += \
+ sources/actions/actionbase.cpp \
+ sources/actions/editsampleaction.cpp \
+ sources/actions/shiftwaveformaction.cpp \
sources/main.cpp \
+ sources/models/actionsmodel.cpp \
+ sources/models/dataplayermodel.cpp \
sources/models/fileworkermodel.cpp \
sources/controls/waveformcontrol.cpp \
sources/core/waveformparser.cpp \
sources/core/wavreader.cpp \
sources/models/parsersettingsmodel.cpp \
sources/models/suspiciouspointsmodel.cpp \
+ sources/models/waveformmodel.cpp \
sources/translations/translationmanager.cpp \
sources/translations/translations.cpp \
sources/util/enummetainfo.cpp \
sources/configuration/configurationmanager.cpp
HEADERS += \
+ sources/actions/actionbase.h \
+ sources/actions/editsampleaction.h \
+ sources/actions/shiftwaveformaction.h \
sources/defines.h \
+ sources/models/actionsmodel.h \
+ sources/models/dataplayermodel.h \
sources/models/fileworkermodel.h \
sources/controls/waveformcontrol.h \
sources/core/waveformparser.h \
sources/core/wavreader.h \
sources/models/parsersettingsmodel.h \
sources/models/suspiciouspointsmodel.h \
+ sources/models/waveformmodel.h \
sources/translations/translationmanager.h \
sources/translations/translations.h \
sources/util/enummetainfo.h \
diff --git a/qml/About.qml b/qml/About.qml
index 0db744e..3bb0da4 100644
--- a/qml/About.qml
+++ b/qml/About.qml
@@ -31,7 +31,7 @@ Dialog {
Text {
id: zxTapeReviverText
- text: 'ZX Tape Reviver %1 (c) 2020-2021 Leonid Golouz'.arg(ConfigurationManager.zxTapeReviverVersion)
+ text: 'ZX Tape Reviver %1 (c) 2020-2022 Leonid Golouz'.arg(ConfigurationManager.zxTapeReviverVersion)
onLinkActivated: Qt.openUrlExternally(link)
}
Text {
diff --git a/qml/DataPlayer.qml b/qml/DataPlayer.qml
new file mode 100644
index 0000000..c8b1100
--- /dev/null
+++ b/qml/DataPlayer.qml
@@ -0,0 +1,235 @@
+//*******************************************************************************
+// ZX Tape Reviver
+//-----------------
+//
+// Author: Leonid Golouz
+// E-mail: lgolouz@list.ru
+// YouTube channel: https://www.youtube.com/channel/UCz_ktTqWVekT0P4zVW8Xgcg
+// YouTube channel e-mail: computerenthusiasttips@mail.ru
+//
+// Code modification and distribution of any kind is not allowed without direct
+// permission of the Author.
+//*******************************************************************************
+
+import QtQuick 2.15
+import QtQuick.Controls 1.3
+import QtQuick.Dialogs 1.3
+import QtQuick.Layouts 1.15
+import QtGraphicalEffects 1.12
+
+import com.models.zxtapereviver 1.0
+import "."
+
+Dialog {
+ id: dataPlayerDialog
+
+ property int selectedChannel: 0
+ property var parsedChannel: undefined
+
+ visible: false
+ title: Translations.id_playing_parsed_data_window_header
+ standardButtons: StandardButton.Close
+ modality: Qt.WindowModal
+ width: 500
+ height: 400
+
+ Connections {
+ target: DataPlayerModel
+ function onCurrentBlockChanged() {
+ var cb = DataPlayerModel.currentBlock;
+ parsedDataView.selection.forEach(function(rowIndex) { parsedDataView.selection.deselect(rowIndex); });
+ parsedDataView.selection.select(cb);
+ }
+ }
+
+ TableView {
+ id: parsedDataView
+
+ width: parent.width
+ //height: parent.height * 0.9
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ bottom: progressBarItem.top
+ bottomMargin: 5
+ }
+
+ TableViewColumn {
+ title: Translations.id_block_number
+ width: rightArea.width * 0.07
+ role: "block"
+ delegate: Item {
+ property bool blkSelected: styleData.value.blockSelected
+ property int blkNumber: styleData.value.blockNumber
+
+ Rectangle {
+ anchors.fill: parent
+ border.width: 0
+ color: parent.blkSelected ? "#A00000FF" : "transparent"
+ Text {
+ anchors.centerIn: parent
+ color: parent.parent.blkSelected ? "white" : "black"
+ text: blkNumber + 1
+ }
+ }
+
+// MouseArea {
+// anchors.fill: parent
+// onClicked: {
+// WaveformParser.toggleBlockSelection(blkNumber);
+// }
+// }
+ }
+ }
+
+ TableViewColumn {
+ title: Translations.id_block_type
+ width: rightArea.width * 0.23
+ role: "blockType"
+ }
+
+ TableViewColumn {
+ title: Translations.id_block_name
+ width: rightArea.width * 0.3
+ role: "blockName"
+ }
+
+ TableViewColumn {
+ title: Translations.id_block_size
+ width: rightArea.width * 0.25
+ role: "blockSize"
+ }
+
+ TableViewColumn {
+ title: Translations.id_block_status
+ width: rightArea.width * 0.45
+ role: "blockStatus"
+ }
+
+ selectionMode: SelectionMode.SingleSelection
+ model: parsedChannel
+ itemDelegate: Text {
+ text: styleData.value
+ color: modelData.state === 0 ? "black" : "red"
+ }
+ }
+
+ Button {
+ id: playParsedData
+
+ text: DataPlayerModel.stopped ? Translations.id_play_parsed_data : Translations.id_stop_playing_parsed_data
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+
+ onClicked: {
+ if (DataPlayerModel.stopped) {
+ DataPlayerModel.playParsedData(selectedChannel, parsedDataView.currentRow === -1 ? 0 : parsedDataView.currentRow);
+ } else {
+ DataPlayerModel.stop();
+ }
+ }
+ }
+
+ Item {
+ id: progressBarItem
+ anchors {
+ bottom:playParsedData.top
+ left: parent.left
+ right: parent.right
+ bottomMargin: 5
+ }
+ height: progressBarRect.height + startDurationText.height
+
+ Rectangle {
+ id: progressBarRect
+
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ }
+ height: textFontMetrics.height * 1.25
+
+ color: "transparent"
+ border.color: "black"
+
+ Rectangle {
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ left: parent.left
+ topMargin: parent.border.width
+ bottomMargin: parent.border.width
+ leftMargin: parent.border.width
+ }
+ width: (parent.width - 2 * parent.border.width) * (DataPlayerModel.processedTime / DataPlayerModel.blockTime)
+ LinearGradient {
+ anchors.fill: parent
+ gradient: Gradient {
+ GradientStop { position: 0.0; color: "#1B94EF" }
+ GradientStop { position: 1.0; color: "#92C1E4" }
+ }
+ start: Qt.point(0, 0)
+ end: Qt.point(parent.width, 0)
+ }
+ }
+
+ Text {
+ id: playingRecordText
+ font.pixelSize: 12
+ anchors.fill: parent
+ verticalAlignment: Text.AlignVCenter
+ horizontalAlignment: Text.AlignHCenter
+ text: {
+ var b = DataPlayerModel.blockData;
+ return b == undefined ? "" : (b.block.blockNumber + 1) + ": " + b.blockType + " " + b.blockName;
+ }
+ }
+ FontMetrics {
+ id: textFontMetrics
+ font: playingRecordText.font
+ }
+ }
+
+ function getTimeText(t) {
+ var bt_s = ~~(t / 1000);
+ var btm = ~~(bt_s / 60);
+ var bts = ~~(bt_s - (btm * 60));
+ return btm + ":" + String(bts).padStart(2, "0");
+ }
+
+ Text {
+ id: startDurationText
+ text: progressBarItem.getTimeText(0)
+ anchors {
+ top: progressBarRect.bottom
+ left: parent.left
+ }
+ }
+ Text {
+ id: endDurationText
+ text: progressBarItem.getTimeText(DataPlayerModel.blockTime)
+ anchors {
+ top: progressBarRect.bottom
+ right: parent.right
+ }
+ }
+ Text {
+ id: currentDurationText
+ text: progressBarItem.getTimeText(DataPlayerModel.processedTime)
+ anchors {
+ top: progressBarRect.bottom
+ left: parent.left
+ right: parent.right
+ }
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+
+ onVisibleChanged: {
+ if (!visible) {
+ DataPlayerModel.stop();
+ }
+ }
+}
diff --git a/qml/ParserSettings.qml b/qml/ParserSettings.qml
index 109401c..9b573db 100644
--- a/qml/ParserSettings.qml
+++ b/qml/ParserSettings.qml
@@ -198,6 +198,7 @@ Dialog {
}
}
CheckBox {
+ id: checkForAbnormalSineCheckbox
anchors.top: grid.bottom
anchors.topMargin: 5
@@ -207,6 +208,20 @@ Dialog {
ParserSettingsModel.checkForAbnormalSine = checked;
}
}
+ Text {
+ id: sineCheckToleranceText
+ anchors.top: checkForAbnormalSineCheckbox.bottom
+ text: Translations.id_sine_check_tolerance
+ visible: checkForAbnormalSineCheckbox.checked
+ }
+ TextField {
+ anchors.top: sineCheckToleranceText.bottom
+ text: ParserSettingsModel.sineCheckTolerance;
+ onTextChanged: {
+ ParserSettingsModel.sineCheckTolerance = text;
+ }
+ visible: checkForAbnormalSineCheckbox.checked
+ }
onReset: {
ParserSettingsModel.restoreDefaultSettings();
diff --git a/qml/Translations.qml b/qml/Translations.qml
index 00bf3b6..9a19eca 100644
--- a/qml/Translations.qml
+++ b/qml/Translations.qml
@@ -25,6 +25,7 @@ QtObject {
property string id_file_menu_item: qsTrId("id_file_menu_item") + TranslationManager.translationChanged
property string id_open_wav_file_menu_item: qsTrId("id_open_wav_file_menu_item") + TranslationManager.translationChanged
property string id_open_waveform_file_menu_item: qsTrId("id_open_waveform_file_menu_item") + TranslationManager.translationChanged
+ property string id_open_tap_file_menu_item: qsTrId("id_open_tap_file_menu_item") + TranslationManager.translationChanged
property string id_save_menu_item: qsTrId("id_save_menu_item") + TranslationManager.translationChanged
property string id_save_parsed_menu_item: qsTrId("id_save_parsed_menu_item") + TranslationManager.translationChanged
property string id_left_channel_menu_item: qsTrId("id_left_channel_menu_item") + TranslationManager.translationChanged
@@ -58,6 +59,7 @@ QtObject {
property string id_check_for_abnormal_sine_when_parsing: qsTrId("id_check_for_abnormal_sine_when_parsing") + TranslationManager.translationChanged
property string id_please_choose_wav_file: qsTrId("id_please_choose_wav_file") + TranslationManager.translationChanged
property string id_please_choose_wfm_file: qsTrId("id_please_choose_wfm_file") + TranslationManager.translationChanged
+ property string id_please_choose_tap_file: qsTrId("id_please_choose_tap_file") + TranslationManager.translationChanged
property string id_wav_files: qsTrId("id_wav_files").arg(filename_wildcard + wav_file_suffix) + TranslationManager.translationChanged
property string id_wfm_files: qsTrId("id_wfm_files").arg(filename_wildcard + wfm_file_suffix) + TranslationManager.translationChanged
property string id_tap_files: qsTrId("id_tap_files").arg(filename_wildcard + tap_file_suffix) + TranslationManager.translationChanged
@@ -94,4 +96,11 @@ QtObject {
property string id_suspicious_point_number: qsTrId("id_suspicious_point_number") + TranslationManager.translationChanged
property string id_suspicious_point_position: qsTrId("id_suspicious_point_position") + TranslationManager.translationChanged
property string id_language_menu_item: qsTrId("id_language_menu_item") + TranslationManager.translationChanged
+ property string id_hotkey_tooltip: qsTrId("id_hotkey_tooltip") + TranslationManager.translationChanged
+ property string id_remove_action: qsTrId("id_remove_action") + TranslationManager.translationChanged //Button caption
+ property string id_action_name: qsTrId("id_action_name") + TranslationManager.translationChanged
+ property string id_sine_check_tolerance: qsTrId("id_sine_check_tolerance") + TranslationManager.translationChanged
+ property string id_play_parsed_data: qsTrId("id_play_parsed_data") + TranslationManager.translationChanged
+ property string id_stop_playing_parsed_data: qsTrId("id_stop_playing_parsed_data") + TranslationManager.translationChanged
+ property string id_playing_parsed_data_window_header: qsTrId("id_playing_parsed_data_window_header") + TranslationManager.translationChanged
}
diff --git a/qml/main.qml b/qml/main.qml
index 3ac726d..d0f8f05 100644
--- a/qml/main.qml
+++ b/qml/main.qml
@@ -13,6 +13,7 @@
import QtQuick 2.15
import QtQuick.Window 2.15
+import QtQuick.Controls 2.5
import QtQuick.Controls 1.4
import QtQuick.Dialogs 1.3
@@ -60,7 +61,7 @@ ApplicationWindow {
text: Translations.id_open_wav_file_menu_item
onTriggered: {
console.log("Opening WAV file");
- openFileDialog.isWavOpening = true;
+ openFileDialog.openDialogType = openFileDialog.openWav;
openFileDialog.open();
}
}
@@ -69,7 +70,16 @@ ApplicationWindow {
text: Translations.id_open_waveform_file_menu_item
onTriggered: {
console.log("Opening Waveform file");
- openFileDialog.isWavOpening = false;
+ openFileDialog.openDialogType = openFileDialog.openWfm;
+ openFileDialog.open();
+ }
+ }
+
+ MenuItem {
+ text: Translations.id_open_tap_file_menu_item
+ onTriggered: {
+ console.log("Opening TAP file");
+ openFileDialog.openDialogType = openFileDialog.openTap;
openFileDialog.open();
}
}
@@ -189,23 +199,50 @@ ApplicationWindow {
FileDialog {
id: openFileDialog
- property bool isWavOpening: true
+ readonly property int openWav: 0
+ readonly property int openWfm: 1
+ readonly property int openTap: 2
+
+ property int openDialogType: openFileDialog.openWav
+
+ title: openDialogType === openFileDialog.openWfm
+ ? Translations.id_please_choose_wfm_file
+ : openDialogType === openFileDialog.openTap
+ ? Translations.id_please_choose_tap_file
+ : Translations.id_please_choose_wav_file
- title: isWavOpening ? Translations.id_please_choose_wav_file : Translations.id_please_choose_wfm_file
selectMultiple: false
sidebarVisible: true
- defaultSuffix: isWavOpening ? Translations.wav_file_suffix : Translations.wfm_file_suffix
- nameFilters: isWavOpening ? [ Translations.id_wav_files ] : [ Translations.id_wfm_files ]
+
+ defaultSuffix: openDialogType === openFileDialog.openWfm
+ ? Translations.wfm_file_suffix
+ : openDialogType === openFileDialog.openTap
+ ? Translations.tap_file_suffix
+ : Translations.wav_file_suffix
+
+ nameFilters: openDialogType === openFileDialog.openWfm
+ ? [ Translations.id_wfm_files ]
+ : openDialogType === openFileDialog.openTap
+ ? [ Translations.id_tap_files ]
+ : [ Translations.id_wav_files ]
onAccepted: {
- var filetype = isWavOpening ? "WAV" : "Waveform";
+ var filetype = openDialogType === openFileDialog.openWfm
+ ? "Waveform"
+ : openDialogType === openFileDialog.openTap
+ ? "TAP"
+ : "WAV";
+
console.log("Selected %1 file: ".arg(filetype) + openFileDialog.fileUrl);
- var res = (isWavOpening
- ? FileWorkerModel.openWavFileByUrl(openFileDialog.fileUrl)
- : FileWorkerModel.openWaveformFileByUrl(openFileDialog.fileUrl));
+ var res = (openDialogType === openFileDialog.openWfm
+ ? FileWorkerModel.openWaveformFileByUrl(openFileDialog.fileUrl)
+ : openDialogType === openFileDialog.openTap
+ ? FileWorkerModel.openTapFileByUrl(openFileDialog.fileUrl)
+ : FileWorkerModel.openWavFileByUrl(openFileDialog.fileUrl));
+
console.log("Open %1 file result: ".arg(filetype) + res);
if (res === 0) {
- if (isWavOpening) {
+ if (openDialogType !== openFileDialog.openWfm) {
SuspiciousPointsModel.clearSuspiciousPoints();
}
restoreWaveformView();
@@ -412,6 +449,26 @@ ApplicationWindow {
}
}
+ Button {
+ id: playParsedData
+
+ text: DataPlayerModel.stopped ? Translations.id_play_parsed_data : Translations.id_stop_playing_parsed_data
+ anchors.top: hZoomOutButton.bottom
+ anchors.right: parent.right
+ anchors.rightMargin: 5
+ anchors.topMargin: hZoomOutButton.anchors.topMargin * 10
+ width: hZoomOutButton.width
+
+ onClicked: {
+ if (DataPlayerModel.stopped) {
+ DataPlayerModel.playParsedData(channelsComboBox.currentIndex, parsedDataView.currentRow === -1 ? 0 : parsedDataView.currentRow);
+ dataPlayerDialog.open();
+ } else {
+ DataPlayerModel.stop();
+ }
+ }
+ }
+
Button {
id: shiftWaveRight
@@ -460,7 +517,26 @@ ApplicationWindow {
Button {
id: reparseButton
- text: Translations.id_reparse
+ function reparse() {
+ }
+
+ Shortcut {
+ id: shortcut_reparseButton
+
+ sequence: "p"
+ autoRepeat: true
+ onActivated: reparseButton.clicked()
+ }
+
+ Shortcut {
+ id: shortcut_reparseButtonShift
+
+ sequence: "Shift+s"
+ autoRepeat: true
+ onActivated: reparseButton.clicked()
+ }
+
+ text: Translations.id_reparse + mainArea.hotkeyHint.arg(shortcut_reparseButton.sequence + " / " + shortcut_reparseButtonShift.sequence)
anchors.top: shiftWaveLeft.bottom
anchors.right: parent.right
anchors.rightMargin: 5
@@ -544,7 +620,9 @@ ApplicationWindow {
width: hZoomOutButton.width
onClicked: {
- getSelectedWaveform().shiftWaveform();
+ ActionsModel.shiftWaveform(1300);
+ waveformControlCh0.update();
+ //getSelectedWaveform().shiftWaveform();
}
}
@@ -679,6 +757,13 @@ ApplicationWindow {
onActivated: toBlockBeginningButton.clicked()
}
+ ToolTip {
+ delay: 1000
+ timeout: 5000
+ visible: toBlockBeginningButton.hovered
+ text: Translations.id_hotkey_tooltip.arg(shortcut_toBlockBeginning.sequence)
+ }
+
text: Translations.id_to_the_beginning_of_the_block
anchors {
top: channelsComboBox.bottom
@@ -712,6 +797,13 @@ ApplicationWindow {
onActivated: toBlockEndButton.clicked()
}
+ ToolTip {
+ delay: 1000
+ timeout: 5000
+ visible: toBlockEndButton.hovered
+ text: Translations.id_hotkey_tooltip.arg(shortcut_toBlockEnd.sequence)
+ }
+
text: Translations.id_to_the_end_of_the_block
anchors {
top: channelsComboBox.bottom
@@ -793,7 +885,7 @@ ApplicationWindow {
TableViewColumn {
title: Translations.id_block_status
- width: rightArea.width * 0.15
+ width: rightArea.width * 0.45
role: "blockStatus"
}
@@ -858,11 +950,13 @@ ApplicationWindow {
anchors {
top: gotoPointButton.bottom
- bottom: parent.bottom
+ //bottom: parent.bottom
left: parent.left
right: parent.right
topMargin: 2
}
+ height: parent.height * 0.25
+ implicitHeight: parent.height * 0.25
selectionMode: SelectionMode.SingleSelection
model: suspiciousPoints
@@ -880,6 +974,54 @@ ApplicationWindow {
width: rightArea.width * 0.9
}
}
+
+ Button {
+ id: removeActionButton
+
+ anchors {
+ top: suspiciousPointsView.bottom
+ left: parent.left
+ right: parent.right
+ leftMargin: 2
+ topMargin: 2
+ }
+
+ text: Translations.id_remove_action
+
+ onClicked: {
+ ActionsModel.removeAction();
+ waveformControlCh0.update();
+ waveformControlCh1.update();
+ }
+ }
+
+ TableView {
+ id: actionsView
+
+ anchors {
+ top: removeActionButton.bottom
+ bottom: parent.bottom
+ left: parent.left
+ right: parent.right
+ topMargin: 2
+ }
+
+ selectionMode: SelectionMode.SingleSelection
+ model: ActionsModel.actions
+ itemDelegate: Text {
+ text: styleData.column === 0 ? styleData.row + 1 : modelData.name
+ }
+
+ TableViewColumn {
+ title: Translations.id_suspicious_point_number
+ width: rightArea.width * 0.1
+ }
+
+ TableViewColumn {
+ title: Translations.id_action_name
+ width: rightArea.width * 0.9
+ }
+ }
}
GoToAddress {
@@ -914,4 +1056,11 @@ ApplicationWindow {
waveformControlCh1.frequency.connect(func);
}
}
+
+ DataPlayer {
+ id: dataPlayerDialog
+
+ selectedChannel: channelsComboBox.currentIndex
+ parsedChannel: channelsComboBox.currentIndex === 0 ? WaveformParser.parsedChannel0 : WaveformParser.parsedChannel1
+ }
}
diff --git a/qml/qml.qrc b/qml/qml.qrc
index fab0996..e521871 100644
--- a/qml/qml.qrc
+++ b/qml/qml.qrc
@@ -7,6 +7,7 @@
ParserSettings.qml
About.qml
Translations.qml
+ DataPlayer.qml
translations/zxtapereviver_en_US.qm
diff --git a/qml/translations/zxtapereviver_en_US.xlf b/qml/translations/zxtapereviver_en_US.xlf
index 27723b4..0f02498 100644
--- a/qml/translations/zxtapereviver_en_US.xlf
+++ b/qml/translations/zxtapereviver_en_US.xlf
@@ -29,6 +29,7 @@
+
@@ -62,6 +63,7 @@
Check for abnormal sine when parsing
Please choose WAV file
Please choose WFM file
+Please choose TAP file
WAV files (%1)
Waveform files (%1)
TAP files (%1)
@@ -105,6 +107,16 @@
English
Russian
+Hotkey: %1
+Remove action
+Action name
+Edit Sample
+Shift Waveform
+Sine Check Tolerance:
+Play parsed data
+Stop playing
+ (Parity: %1 ; Should be: %2)
+