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 @@ FileFile Open WAV file...Open WAV file... Open Waveform file...Open Waveform file... +Open TAP file...Open TAP file... SaveSave ParsedParsed Left channel...Left channel... @@ -62,6 +63,7 @@ Check for abnormal sine when parsingCheck for abnormal sine when parsing Please choose WAV filePlease choose WAV file Please choose WFM filePlease choose WFM file +Please choose TAP filePlease choose TAP file WAV files (%1)WAV files (%1) Waveform files (%1)Waveform files (%1) TAP files (%1)TAP files (%1) @@ -105,6 +107,16 @@ LanguageLanguage EnglishEnglish RussianRussian +Hotkey: %1Hotkey: %1 +Remove actionRemove action +Action nameAction name +Edit SampleEdit Sample +Shift WaveformShift Waveform +Sine Check Tolerance:Sine Check Tolerance: +Play parsed dataPlay parsed data +Stop playingStop playing + (Parity: %1 ; Should be: %2) (Parity: %1 ; Should be: %2) +Playing parsed dataPlaying parsed data diff --git a/qml/translations/zxtapereviver_ru_RU.xlf b/qml/translations/zxtapereviver_ru_RU.xlf index d4f3d02..d70d91a 100644 --- a/qml/translations/zxtapereviver_ru_RU.xlf +++ b/qml/translations/zxtapereviver_ru_RU.xlf @@ -29,6 +29,7 @@ FileФайл Open WAV file...Открыть WAV файл... Open Waveform file...Открыть файл с формой волны... +Open TAP file...Открыть TAP файл... SaveСохранить ParsedРазобранный Left channel...Левый канал... @@ -62,6 +63,7 @@ Check for abnormal sine when parsingПроверка на явно вырожденную синусоиду во время разбора Please choose WAV fileПожалуйста, выберите WAV файл Please choose WFM fileПожалуйста, выберите WFM файл +Please choose TAP fileПожалуйста, выберите TAP WAV files (%1)Файлы WAV (%1) Waveform files (%1)Файлы формы волны (%1) TAP files (%1)Файлы TAP (%1) @@ -105,6 +107,16 @@ LanguageЯзык EnglishАнглийский RussianРусский +Hotkey: %1Горячая клавиша: %1 +Remove actionУдалить действие +Action nameНазвание действия +Edit SampleРедактирование семпла +Shift WaveformСдвиг волны +Sine Check Tolerance:Допуск при проверке синусоиды: +Play parsed dataИграть разобранное +Stop playingСтоп воспроизведения + (Parity: %1 ; Should be: %2) (Сумма: %1 ; Ожидается: %2) +Playing parsed dataВоспроизведение разобранных данных diff --git a/sources/actions/actionbase.cpp b/sources/actions/actionbase.cpp new file mode 100644 index 0000000..64152fb --- /dev/null +++ b/sources/actions/actionbase.cpp @@ -0,0 +1,33 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + +#include "actionbase.h" + +ActionBase::ActionBase(int channel, const QString& name) : + m_channel(channel), + m_actionName(name) +{ + +} + +int ActionBase::channel() const { + return m_channel; +} + +const QString& ActionBase::actionName() const { + return m_actionName; +} + +bool ActionBase::isActionValid(const QSharedPointer& wf) const { + return !wf.isNull(); +} diff --git a/sources/actions/actionbase.h b/sources/actions/actionbase.h new file mode 100644 index 0000000..0788505 --- /dev/null +++ b/sources/actions/actionbase.h @@ -0,0 +1,40 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + +#ifndef ACTIONBASE_H +#define ACTIONBASE_H + +#include "sources/defines.h" +#include +#include + +class ActionBase +{ + const int m_channel; + const QString m_actionName; + +public: + ActionBase(int channel, const QString& name = { }); + virtual ~ActionBase() = default; + + int channel() const; + const QString& actionName() const; + + virtual bool apply() = 0; + virtual void undo() = 0; + +protected: + virtual bool isActionValid(const QSharedPointer& wf) const; +}; + +#endif // ACTIONBASE_H diff --git a/sources/actions/editsampleaction.cpp b/sources/actions/editsampleaction.cpp new file mode 100644 index 0000000..1061822 --- /dev/null +++ b/sources/actions/editsampleaction.cpp @@ -0,0 +1,42 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + +#include "editsampleaction.h" +#include "sources/models/waveformmodel.h" +#include "sources/translations/translations.h" + +EditSampleAction::EditSampleAction(int channel, const EditSampleActionParams& params) : + ActionBase(channel, qtTrId(ID_EDIT_ACTION)), + m_params(params) +{ + +} + +bool EditSampleAction::apply() { + auto wf { WaveFormModel::instance()->getChannel(channel()) }; + const bool valid { isActionValid(wf) }; + if (valid) { + wf->operator[](m_params.sample) = m_params.newValue; + } + + return valid; +} + +void EditSampleAction::undo() { + auto wf { WaveFormModel::instance()->getChannel(channel()) }; + wf->operator[](m_params.sample) = m_params.previousValue; +} + +bool EditSampleAction::isActionValid(const QSharedPointer& wf) const { + return ActionBase::isActionValid(wf) && m_params.sample >= 0 && m_params.sample < wf->size(); +} diff --git a/sources/actions/editsampleaction.h b/sources/actions/editsampleaction.h new file mode 100644 index 0000000..3eb0ffe --- /dev/null +++ b/sources/actions/editsampleaction.h @@ -0,0 +1,40 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + +#ifndef EDITSAMPLEACTION_H +#define EDITSAMPLEACTION_H + +#include "actionbase.h" + +struct EditSampleActionParams { + QWavVectorType previousValue; + QWavVectorType newValue; + int sample; +}; + +class EditSampleAction : public ActionBase +{ + const EditSampleActionParams m_params; + +public: + EditSampleAction(int channel, const EditSampleActionParams& params); + virtual ~EditSampleAction() = default; + + virtual bool apply() override; + virtual void undo() override; + +private: + virtual bool isActionValid(const QSharedPointer& wf) const; +}; + +#endif // EDITSAMPLEACTION_H diff --git a/sources/actions/shiftwaveformaction.cpp b/sources/actions/shiftwaveformaction.cpp new file mode 100644 index 0000000..ba78998 --- /dev/null +++ b/sources/actions/shiftwaveformaction.cpp @@ -0,0 +1,41 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + +#include "shiftwaveformaction.h" +#include "sources/models/waveformmodel.h" +#include "sources/translations/translations.h" + +ShiftWaveFormAction::ShiftWaveFormAction(int channel, const ShiftWaveFormActionParams& params) : + ActionBase(channel, qtTrId(ID_SHIFT_WAVEFORM_ACTION)), + m_params(params) +{ + +} + +bool ShiftWaveFormAction::apply() { + auto wf { WaveFormModel::instance()->getChannel(channel()) }; + const bool valid { isActionValid(wf) }; + if (valid) { + std::for_each(wf->begin(), wf->end(), [this](QWavVectorType& itm) { + itm += m_params.offsetValue; + }); + } + return valid; +} + +void ShiftWaveFormAction::undo() { + auto wf { WaveFormModel::instance()->getChannel(channel()) }; + std::for_each(wf->begin(), wf->end(), [this](QWavVectorType& itm) { + itm -= m_params.offsetValue; + }); +} diff --git a/sources/actions/shiftwaveformaction.h b/sources/actions/shiftwaveformaction.h new file mode 100644 index 0000000..95e4a84 --- /dev/null +++ b/sources/actions/shiftwaveformaction.h @@ -0,0 +1,35 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + +#ifndef SHIFTWAVEFORMACTION_H +#define SHIFTWAVEFORMACTION_H + +#include "actionbase.h" + +struct ShiftWaveFormActionParams { + QWavVectorType offsetValue; +}; + +class ShiftWaveFormAction : public ActionBase +{ + const ShiftWaveFormActionParams m_params; + +public: + ShiftWaveFormAction(int channel, const ShiftWaveFormActionParams& params); + virtual ~ShiftWaveFormAction() = default; + + virtual bool apply() override; + virtual void undo() override; +}; + +#endif // SHIFTWAVEFORMACTION_H diff --git a/sources/configuration/configurationmanager.cpp b/sources/configuration/configurationmanager.cpp index f4b49f8..2d1d3c0 100644 --- a/sources/configuration/configurationmanager.cpp +++ b/sources/configuration/configurationmanager.cpp @@ -1,3 +1,16 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + #include "configurationmanager.h" #include #include diff --git a/sources/controls/waveformcontrol.cpp b/sources/controls/waveformcontrol.cpp index d2f3140..1ad63bf 100644 --- a/sources/controls/waveformcontrol.cpp +++ b/sources/controls/waveformcontrol.cpp @@ -22,11 +22,14 @@ #include #include #include "sources/translations/translations.h" +#include "sources/models/actionsmodel.h" +#include "sources/actions/editsampleaction.h" WaveformControl::WaveformControl(QQuickItem* parent) : QQuickPaintedItem(parent), mWavReader(*WavReader::instance()), mWavParser(*WaveformParser::instance()), + mWaveFormModel(*WaveFormModel::instance()), m_customData(*ConfigurationManager::instance()->getWaveformCustomization()), m_channelNumber(0), m_isWaveformRepaired(false), @@ -56,10 +59,9 @@ QColor WaveformControl::getBackgroundColor() const { } } -QWavVector* WaveformControl::getChannel(uint* chNum) const -{ +QSharedPointer WaveformControl::getChannel(uint* chNum) const { const auto c = chNum ? *chNum : m_channelNumber; - return c == 0 ? mWavReader.getChannel0() : mWavReader.getChannel1(); + return mWaveFormModel.getChannel(c); } int WaveformControl::getWavPositionByMouseX(int x, int* point, double* dx) const @@ -75,8 +77,7 @@ int WaveformControl::getWavPositionByMouseX(int x, int* point, double* dx) const return rpoint + getWavePos() * xinc; } -void WaveformControl::paint(QPainter* painter) -{ +void WaveformControl::paint(QPainter* painter) { auto p = painter->pen(); painter->setBackground(QBrush(getBackgroundColor())); painter->setBackgroundMode(Qt::OpaqueMode); @@ -88,7 +89,7 @@ void WaveformControl::paint(QPainter* painter) painter->drawLine(0, halfHeight, bRect.width(), halfHeight); int32_t scale = bRect.width() * getXScaleFactor(); int32_t pos = getWavePos(); - const auto* channel = getChannel(); + const auto channel = getChannel(); if (channel == nullptr) { return; } @@ -319,12 +320,14 @@ void WaveformControl::mousePressEvent(QMouseEvent* event) else { if (dpoint >= (event->x() - dx/2) && dpoint <= (event->x() + dx/2)) { const double maxy = getYScaleFactor(); - double y = halfHeight - ((double) (getChannel()->operator[](m_clickPosition)) / maxy) * waveHeight; + auto initialVal { getChannel()->operator[](m_clickPosition) }; + double y = halfHeight - ((double) (initialVal) / maxy) * waveHeight; if (!m_customData.checkVerticalRange() || (y >= event->y() - 2 && y <= event->y() + 2)) { if (event->button() == Qt::LeftButton) { m_pointIndex = point; + m_initialValue = initialVal; m_pointGrabbed = true; - qDebug() << "Grabbed point: " << getChannel()->operator[](m_clickPosition); + qDebug() << "Grabbed point: " << initialVal; //getChannel()->operator[](m_clickPosition); } else { if (QGuiApplication::queryKeyboardModifiers() != Qt::ShiftModifier) { @@ -359,7 +362,7 @@ void WaveformControl::mouseReleaseEvent(QMouseEvent* event) } if (m_operationMode == WaveformRepairMode || m_operationMode == WaveformSelectionMode) { - if ( m_clickState == WaitForFirstRelease && m_clickTime.msecsTo(now) <= 500) { + if (m_clickState == WaitForFirstRelease && m_clickTime.msecsTo(now) <= 500) { m_clickState = WaitForSecondPress; } else if (m_clickState == WaitForSecondRelease && m_clickTime.msecsTo(now) <= 500) { @@ -369,8 +372,11 @@ void WaveformControl::mouseReleaseEvent(QMouseEvent* event) else { m_clickState = WaitForFirstPress; } - } - else if (m_operationMode == WaveformMeasurementMode) { + + if (m_pointGrabbed) { + ActionsModel::instance()->addAction(QSharedPointer::create(m_channelNumber, EditSampleActionParams { m_initialValue, m_newValue, m_clickPosition })); + } + } else if (m_operationMode == WaveformMeasurementMode) { auto& clickPoint = m_clickCount == 0 ? m_selectionRange.first : m_selectionRange.second; clickPoint = getWavPositionByMouseX(event->x()); if (m_clickCount == 1) { @@ -410,7 +416,7 @@ void WaveformControl::mouseMoveEvent(QMouseEvent* event) } } else if (m_operationMode == WaveformRepairMode) { - const auto* ch = getChannel(); + const auto ch = getChannel(); if (!ch) { return; } @@ -420,6 +426,7 @@ void WaveformControl::mouseMoveEvent(QMouseEvent* event) const auto pointerPos = halfHeight - event->y(); double val = halfHeight + (m_yScaleFactor / waveHeight * pointerPos); if (m_pointIndex + getWavePos() >= 0 && m_pointIndex + getWavePos() < ch->size()) { + m_newValue = val; getChannel()->operator[](m_pointIndex + getWavePos()) = val; } qDebug() << "Setting point: " << m_pointIndex + getWavePos(); @@ -432,7 +439,7 @@ void WaveformControl::mouseMoveEvent(QMouseEvent* event) // Smooth drawing at Repair mode case Qt::RightButton: { if (m_operationMode == WaveformRepairMode && QGuiApplication::queryKeyboardModifiers() == Qt::ShiftModifier) { - const auto* ch = getChannel(); + const auto ch = getChannel(); if (!ch) { return; } @@ -489,8 +496,8 @@ void WaveformControl::saveWaveform() void WaveformControl::repairWaveform() { if (!m_isWaveformRepaired) { - //mWavReader.repairWaveform(m_channelNumber); - mWavReader.normalizeWaveform2(m_channelNumber); + mWavReader.repairWaveform(m_channelNumber); + //mWavReader.normalizeWaveform2(m_channelNumber); update(); m_isWaveformRepaired = true; emit isWaveformRepairedChanged(); diff --git a/sources/controls/waveformcontrol.h b/sources/controls/waveformcontrol.h index b26f26c..63d9cfd 100644 --- a/sources/controls/waveformcontrol.h +++ b/sources/controls/waveformcontrol.h @@ -19,6 +19,7 @@ #include "sources/core/wavreader.h" #include "sources/core/waveformparser.h" #include "sources/configuration/configurationmanager.h" +#include "sources/models/waveformmodel.h" #include "sources/util/enummetainfo.h" class WaveformControl : public QQuickPaintedItem @@ -35,6 +36,7 @@ class WaveformControl : public QQuickPaintedItem WavReader& mWavReader; WaveformParser& mWavParser; + WaveFormModel& mWaveFormModel; ConfigurationManager::WaveformCustomization& m_customData; QColor getBackgroundColor() const; @@ -102,6 +104,8 @@ class WaveformControl : public QQuickPaintedItem bool m_allowToGrabPoint; bool m_pointGrabbed; int m_pointIndex; + QWavVectorType m_initialValue; + QWavVectorType m_newValue; int m_wavePos; double m_xScaleFactor; double m_yScaleFactor; @@ -113,7 +117,7 @@ class WaveformControl : public QQuickPaintedItem QPair m_selectionRange; int m_clickCount; - QWavVector* getChannel(uint* chNum = nullptr) const; + QSharedPointer getChannel(uint* chNum = nullptr) const; int getWavPositionByMouseX(int x, int* point = nullptr, double* dx = nullptr) const; }; diff --git a/sources/core/waveformparser.cpp b/sources/core/waveformparser.cpp index 26dc061..afbb0b9 100644 --- a/sources/core/waveformparser.cpp +++ b/sources/core/waveformparser.cpp @@ -57,8 +57,8 @@ void WaveformParser::parse(uint chNum) const auto& parserSettings = ParserSettingsModel::instance()->getParserSettings(); auto isSineNormal = [&parserSettings, sampleRate](const WaveformPart& b, const WaveformPart& e, bool zeroCheck) -> bool { if (parserSettings.checkForAbnormalSine) { - return isFreqFitsInDelta(sampleRate, b.length, zeroCheck ? parserSettings.zeroHalfFreq : parserSettings.oneHalfFreq, zeroCheck ? parserSettings.zeroDelta : parserSettings.oneDelta, 0.5) && - isFreqFitsInDelta(sampleRate, e.length, zeroCheck ? parserSettings.zeroHalfFreq : parserSettings.oneHalfFreq, zeroCheck ? parserSettings.zeroDelta : parserSettings.oneDelta, 0.5); + return isFreqFitsInDelta(sampleRate, b.length, zeroCheck ? parserSettings.zeroHalfFreq : parserSettings.oneHalfFreq, zeroCheck ? parserSettings.zeroDelta : parserSettings.oneDelta, parserSettings.sineCheckTolerance) && + isFreqFitsInDelta(sampleRate, e.length, zeroCheck ? parserSettings.zeroHalfFreq : parserSettings.oneHalfFreq, zeroCheck ? parserSettings.zeroDelta : parserSettings.oneDelta, parserSettings.sineCheckTolerance); } return true; }; @@ -137,7 +137,7 @@ void WaveformParser::parse(uint chNum) it = std::next(it); if (it != parsed.end()) { const auto len = it->length + prevIt->length; - if (isFreqFitsInDelta(sampleRate, len, parserSettings.zeroFreq, parserSettings.zeroDelta) && isSineNormal(*prevIt, *it, true)) { //ZERO + if (isFreqFitsInDelta2(sampleRate, len, parserSettings.zeroFreq, parserSettings.zeroDelta, 0.75) && isSineNormal(*prevIt, *it, true)) { //ZERO fillParsedWaveform(chNum, *prevIt, zeroBit ^ sequenceMiddle); fillParsedWaveform(chNum, *it, zeroBit ^ sequenceMiddle); parsedWaveform[prevIt->begin] = zeroBit ^ sequenceBegin; @@ -165,7 +165,7 @@ void WaveformParser::parse(uint chNum) bit ^= bit; } } - else if (isFreqFitsInDelta(sampleRate, len, parserSettings.oneFreq, parserSettings.oneDelta) && isSineNormal(*prevIt, *it, false)) { //ONE + else if (isFreqFitsInDelta2(sampleRate, len, parserSettings.oneFreq, 0.75, parserSettings.oneDelta) && isSineNormal(*prevIt, *it, false)) { //ONE fillParsedWaveform(chNum, *prevIt, oneBit ^ sequenceMiddle); fillParsedWaveform(chNum, *it, oneBit ^ sequenceMiddle); parsedWaveform[prevIt->begin] = oneBit ^ sequenceBegin; @@ -198,9 +198,11 @@ void WaveformParser::parse(uint chNum) db.data = data; db.waveformData = waveformData; parity ^= data.last(); //Removing parity byte from overal parity check sum - db.state = parity == data.last() ? DataState::OK : DataState::R_TAPE_LOADING_ERROR; //Should be checked with checksum + //Storing parity data + db.parityAwaited = data.last(); + db.parityCalculated = parity; + db.state = parity == db.parityAwaited ? DataState::OK : DataState::R_TAPE_LOADING_ERROR; //Should be checked with checksum parity ^= parity; //Zeroing parity byte - parsedData.append(db); } } @@ -213,7 +215,10 @@ void WaveformParser::parse(uint chNum) db.data = data; db.waveformData = waveformData; parity ^= data.last(); //Remove parity byte from overal parity check sum - db.state = parity == data.last() ? DataState::OK : DataState::R_TAPE_LOADING_ERROR; //Should be checked with checksum + //Storing parity data + db.parityAwaited = data.last(); + db.parityCalculated = parity; + db.state = parity == db.parityAwaited ? DataState::OK : DataState::R_TAPE_LOADING_ERROR; //Should be checked with checksum parity ^= parity; //Zeroing parity byte parsedData.append(db); @@ -244,7 +249,8 @@ void WaveformParser::parse(uint chNum) void WaveformParser::saveTap(uint chNum, const QString& fileName) { QFile f(fileName.isEmpty() ? QString("tape_%1_%2.tap").arg(QDateTime::currentDateTime().toString("dd.MM.yyyy hh-mm-ss.zzz")).arg(chNum ? "R" : "L") : fileName); - f.open(QIODevice::ReadWrite); + f.remove(); //Remove file if exists + f.open(QIODevice::WriteOnly); auto& parsedData = mParsedData[chNum]; for (auto i = 0; i < parsedData.size(); ++i) { @@ -271,11 +277,14 @@ void WaveformParser::fillParsedWaveform(uint chNum, const WaveformPart& p, uint8 } } -QVector WaveformParser::getParsedWaveform(uint chNum) const -{ +QVector WaveformParser::getParsedWaveform(uint chNum) const { return mParsedWaveform[chNum]; } +QPair, QVector> WaveformParser::getParsedData(uint chNum) const { + return { mParsedData[chNum], mSelectedBlocks }; +} + void WaveformParser::toggleBlockSelection(int blockNum) { if (blockNum < mSelectedBlocks.size()) { auto& blk = mSelectedBlocks[blockNum]; @@ -288,7 +297,7 @@ void WaveformParser::toggleBlockSelection(int blockNum) { int WaveformParser::getBlockDataStart(uint chNum, uint blockNum) const { - if (chNum < mParsedData.size() && blockNum < mParsedData[chNum].size()) { + if (chNum < (unsigned) mParsedData.size() && blockNum < (unsigned) mParsedData[chNum].size()) { return mParsedData[chNum][blockNum].dataStart; } return 0; @@ -296,7 +305,7 @@ int WaveformParser::getBlockDataStart(uint chNum, uint blockNum) const int WaveformParser::getBlockDataEnd(uint chNum, uint blockNum) const { - if (chNum < mParsedData.size() && blockNum < mParsedData[chNum].size()) { + if (chNum < (unsigned) mParsedData.size() && blockNum < (unsigned) mParsedData[chNum].size()) { return mParsedData[chNum][blockNum].dataEnd; } return 0; @@ -304,7 +313,7 @@ int WaveformParser::getBlockDataEnd(uint chNum, uint blockNum) const int WaveformParser::getPositionByAddress(uint chNum, uint blockNum, uint addr) const { - if (chNum < mParsedData.size() && blockNum < mParsedData[chNum].size() && addr < mParsedData[chNum][blockNum].waveformData.size()) { + if (chNum < (unsigned) mParsedData.size() && blockNum < (unsigned) mParsedData[chNum].size() && addr < (unsigned) mParsedData[chNum][blockNum].waveformData.size()) { return mParsedData[chNum][blockNum].waveformData.at(addr).end; } return 0; @@ -355,19 +364,11 @@ QVariantList WaveformParser::getParsedChannelData(uint chNum) const m.insert("blockSize", sizeText); QString nameText; if (blockType >= 0) { - const auto loopRange { std::min( -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - (int) -#else - (qsizetype) -#endif - 12, i.data.size()) - }; - + const auto loopRange { std::min(decltype(i.data.size())(12), i.data.size()) }; nameText = QByteArray((const char*) &i.data.data()[2], loopRange > 1 ? loopRange - 2 : 0); } m.insert("blockName", nameText); - m.insert("blockStatus", i.state == OK ? id_ok : id_error); + m.insert("blockStatus", (i.state == OK ? id_ok : id_error) + qtTrId(ID_PARITY_MESSAGE).arg(QString::number(i.parityCalculated, 16).toUpper().rightJustified(2, '0')).arg(QString::number(i.parityAwaited, 16).toUpper().rightJustified(2, '0'))); m.insert("state", i.state); } else { diff --git a/sources/core/waveformparser.h b/sources/core/waveformparser.h index a3f2c91..2ca2c18 100644 --- a/sources/core/waveformparser.h +++ b/sources/core/waveformparser.h @@ -50,18 +50,16 @@ class WaveformParser : public QObject // | | | | | | | |----------------- 1 - end of signal sequence // x x x x x x x x -const uint8_t zeroBit = 0b10000000; //zero data bit -const uint8_t oneBit = 0b01000000; //one bit -const uint8_t pilotTone = 0b00100000; //pilot tone -const uint8_t synchroSignal = 0b00010000; //synchro signal -const uint8_t byteBound = 0b00001000; //byte bound -const uint8_t sequenceBegin = 0b00000100; //begin of signal sequence -const uint8_t sequenceMiddle = 0b00000010; //middle of signal sequence -const uint8_t sequenceEnd = 0b00000001; //end of signal sequence + const uint8_t zeroBit = 0b10000000; //zero data bit + const uint8_t oneBit = 0b01000000; //one bit + const uint8_t pilotTone = 0b00100000; //pilot tone + const uint8_t synchroSignal = 0b00010000; //synchro signal + const uint8_t byteBound = 0b00001000; //byte bound + const uint8_t sequenceBegin = 0b00000100; //begin of signal sequence + const uint8_t sequenceMiddle = 0b00000010; //middle of signal sequence + const uint8_t sequenceEnd = 0b00000001; //end of signal sequence -private: enum WaveformSign { POSITIVE, NEGATIVE }; - enum StateType { SEARCH_OF_PILOT_TONE, PILOT_TONE, SYNCHRO_SIGNAL, DATA_SIGNAL, END_OF_DATA, NO_MORE_DATA }; enum DataState { OK, R_TAPE_LOADING_ERROR }; struct WaveformPart @@ -79,8 +77,13 @@ const uint8_t sequenceEnd = 0b00000001; //end of signal sequence QVector data; QVector waveformData; DataState state; + uint8_t parityCalculated; + uint8_t parityAwaited; }; +private: + enum StateType { SEARCH_OF_PILOT_TONE, PILOT_TONE, SYNCHRO_SIGNAL, DATA_SIGNAL, END_OF_DATA, NO_MORE_DATA }; + template QVector parseChannel(const QVector& ch) { QVector result; @@ -90,7 +93,7 @@ const uint8_t sequenceEnd = 0b00000001; //end of signal sequence auto it = ch.begin(); auto val = *it; - while(it != ch.end()) { + while (it != ch.end()) { auto prevIt = it; it = std::find_if(it, ch.end(), [&val](const T& i) { return lessThanZero(val) != lessThanZero(i); }); WaveformPart part; @@ -129,6 +132,7 @@ const uint8_t sequenceEnd = 0b00000001; //end of signal sequence void saveTap(uint chNum, const QString& fileName = QString()); void saveWaveform(uint chNum); QVector getParsedWaveform(uint chNum) const; + QPair, QVector> getParsedData(uint chNum) const; Q_INVOKABLE void toggleBlockSelection(int blockNum); Q_INVOKABLE int getBlockDataStart(uint chNum, uint blockNum) const; diff --git a/sources/core/wavreader.cpp b/sources/core/wavreader.cpp index c510fb5..a617da9 100644 --- a/sources/core/wavreader.cpp +++ b/sources/core/wavreader.cpp @@ -14,10 +14,12 @@ #include "wavreader.h" #include "sources/models/suspiciouspointsmodel.h" #include "sources/models/parsersettingsmodel.h" +#include "sources/models/waveformmodel.h" #include #include #include #include +#include WavReader::WavReader(QObject* parent) : QObject(parent), @@ -168,6 +170,7 @@ WavReader::ErrorCodesEnum WavReader::read() } } + WaveFormModel::instance()->initialize({ getChannel0(), getChannel1() }); emit numberOfChannelsChanged(); return Ok; } @@ -187,14 +190,14 @@ uint WavReader::getBytesPerSample() const return mWavOpened ? mWavFormatHeader.significantBitsPerSample / 8 : 0; } -QWavVector* WavReader::getChannel0() const +QSharedPointer WavReader::getChannel0() const { - return mChannel0.get(); + return mChannel0; } -QWavVector* WavReader::getChannel1() const +QSharedPointer WavReader::getChannel1() const { - return mChannel1.get(); + return mChannel1; } WavReader::ErrorCodesEnum WavReader::close() @@ -236,6 +239,142 @@ void WavReader::loadWaveform(const QString& fname) SuspiciousPointsModel::instance()->setSuspiciousPoints(sp); f.close(); + WaveFormModel::instance()->initialize({ getChannel0(), getChannel1() }); + mWavOpened = true; +} + +unsigned WavReader::calculateOnesInByte (uint8_t n) { + n = ((n>>1) & 0x55) + (n & 0x55); + n = ((n>>2) & 0x33) + (n & 0x33); + n = ((n>>4) & 0x0F) + (n & 0x0F); + return n; +} + +void WavReader::loadTap(const QString& fname) { + QFile f(fname); + f.open(QIODevice::ReadOnly); + const auto guard = qScopeGuard([&f](){ f.close(); }); + + const size_t fSize = f.size(); + QByteArray b(f.read(fSize)); + //Create header + mWavFormatHeader = WavFmt { + WavChunk { 0, 0 }, //Doesn't matter for TAP + 1, //Compression code + 2, //Number of channels + 48000, //Sample Rate + 192000, //Avg bytes per second + 4, //Block align + 16 //Significant bits per sample + }; + + const auto& parserSettings = ParserSettingsModel::instance()->getParserSettings(); + + const auto oneFreq { parserSettings.oneHalfFreq / 2 }; + const auto oneHalfFreq { oneFreq * 2 }; + const auto zeroFreq { parserSettings.zeroHalfFreq / 2 }; + const auto zeroHalfFreq { zeroFreq * 2 }; + const auto synchroFirstHalf { parserSettings.synchroFirstHalfFreq }; + const auto synchroSecondHalf { parserSettings.synchroSecondHalfFreq }; + const auto pilotFreq { parserSettings.pilotHalfFreq / 2 }; + const auto pilotHalfFreq { pilotFreq * 2 }; + const auto silence { 0.5 }; + const auto pilotLen { 3 }; + + //Check TAP file for correctness + size_t pos { 0 }; + bool err { false }; + size_t wavlen { 0 }; + while (!err && pos < fSize) { + err = fSize <= pos + sizeof(uint16_t); + if (err) { + continue; + } + + const uint16_t blockSize { *getData(b, pos) }; + err = fSize < pos + blockSize; + if (err) { + continue; + } + + //Pilot + wavlen += mWavFormatHeader.sampleRate * pilotLen; + //Synchro + wavlen += mWavFormatHeader.sampleRate / synchroFirstHalf; + wavlen += mWavFormatHeader.sampleRate / synchroSecondHalf; + + for (size_t i { 0 }; i < blockSize; ++i) { + const uint8_t byte { *getData(b, pos) }; + const auto ones { calculateOnesInByte(byte) }; + const size_t byteLen { ones * (mWavFormatHeader.sampleRate / oneFreq) + (8 - ones) * (mWavFormatHeader.sampleRate / zeroFreq) }; + + wavlen += byteLen; + } + + //Silence + wavlen += mWavFormatHeader.sampleRate * silence; + } + if (err) { + return; + } + + QWavVector v(wavlen, -1.); + size_t wavpos { 0 }; + pos ^= pos; + while (pos < fSize) { + const uint16_t blockSize { *getData(b, pos) }; + + //Pilot + wavlen = mWavFormatHeader.sampleRate / pilotHalfFreq; + auto threshold { mWavFormatHeader.sampleRate * pilotLen / (wavlen * 2) }; + for (size_t i { 0 }; i < threshold; ++i) { + for (auto p { 0 }; p <= 1; ++p) { + const QWavVectorType val { QWavVectorType(32767 * (p ? 1 : -1)) }; + for (size_t c { 0 }; c < wavlen; ++c) { + v[wavpos++] = val; + } + } + } + //Synchro + for (auto w { 0 }; w <= 1; ++w) { + wavlen = mWavFormatHeader.sampleRate / (w ? synchroSecondHalf : synchroFirstHalf); + const QWavVectorType val { QWavVectorType(32767 * (w ? 1 : -1)) }; + for (size_t c { 0 }; c < wavlen; ++c) { + v[wavpos++] = val; + } + } + + for (size_t i { 0 }; i < blockSize; ++i) { + const uint8_t byte { *getData(b, pos) }; + //const auto ones { calculateOnesInByte(byte) }; + //const size_t byteLen { ones * (mWavFormatHeader.sampleRate / oneFreq) + (8 - ones) * (mWavFormatHeader.sampleRate / zeroFreq) }; + for (int i { 7 }; i >= 0; --i) { + const uint8_t bit8 = 1 << i; + const auto bit { byte & bit8 }; + wavlen = mWavFormatHeader.sampleRate / (bit == 0 ? zeroHalfFreq : oneHalfFreq); + for (auto b { 0 }; b <= 1; ++b) { + const QWavVectorType val { QWavVectorType(32767 * (b ? 1 : -1)) }; + for (size_t c { 0 }; c < wavlen; ++c) { + v[wavpos++] = val; + } + } + } + } + + //Silence + threshold = mWavFormatHeader.sampleRate * silence; + for (size_t s { 0 }; s < threshold; ++s) { + v[wavpos++] = s == 1 ? 0. : -1.; + } + } + + + for (auto i = 0; i < mWavFormatHeader.numberOfChannels; ++i) { + auto& ch = i == 0 ? mChannel0 : mChannel1; + ch.reset(new QWavVector(v)); + } + + WaveFormModel::instance()->initialize({ getChannel0(), getChannel1() }); mWavOpened = true; } @@ -314,6 +453,167 @@ void WavReader::restoreWaveform(uint chNum) ch.reset(new QWavVector(*mStoredChannels[chNum])); } +void WavReader::repairWaveform(uint chNum) { + if (chNum >= mWavFormatHeader.numberOfChannels) { + qDebug() << "Channel number exceeds number of channels"; + return; + } + + auto& ch = *(chNum == 0 ? mChannel0 : mChannel1).get(); + auto& ch2 = *(chNum == 0 ? mChannel1 : mChannel0).get(); + const auto size { ch.size() }; + const auto size2 { ch2.size() }; + if (size == 0) { + qDebug() << "Empty channel data"; + return; + } + for (auto i = 0; i < size; ++i) { + const auto u = i < size2 ? (ch[i] + ch2[i]) / 2 : ch[i]; + ch[i] = u; + } + return; + + auto siz1 { ch.size() }; + + int du,siz0,_siz0; // last amplitude value, size of last buff + int a0,u0,du0; // peak (index,amplitude,delta) + int a1,u1,du1; // peak (index,amplitude,delta) + int a2,u2,du2; // peak (index,amplitude,delta) + + du=0x7FFFFFFF; siz0=0; _siz0=siz1; + a0=0; u0=0x7FFFFFFF; du0=0; + a1=0; u1=0x7FFFFFFF; du1=0; + a2=0; u2=0x7FFFFFFF; du2=0; + + __int64 adr,/*i,*/u,thr,A0,A1,U0,U1,uu; + // noise amplitude + // threshold min max + //if (wav.fmt.bits== 8){ thr= 32; U0= 0; U1= 255; } + { thr=6000; U0=-30000; U1=+30000; } + /* + for (adr=0;adr=0) du2+=du; // no peak + else{ + if ((abs(du1)>thr)&&(abs(du2)>thr)) // 2 valid peaks + { + uu=u; + // center and normalize amplitude + if (u1thr) + for (;a1U1) u=U1; + ch[a1] = u; +// for (i=0;i=0)&&(u0!=0x7FFFFFFF)) a0-=siz1; +// if ((a1>=0)&&(u1!=0x7FFFFFFF)) a1-=siz1; +// if ((a2>=0)&&(u2!=0x7FFFFFFF)) a2-=siz1; +// siz0=_siz0; _siz0=siz1; +*/ + const auto thrhold = thr; + auto v { ch.first() }; + auto max { v }; + //auto it { ch.begin() }; + auto i { size - size }; + while (i < size) { + v = ch[i]; + while (i < size) { + //auto tit = std::find_if(it, ch.end(), [&max, v, threshold](auto& val) { + auto& val = ch[i]; + bool signEqual { lessThanZero(v) == lessThanZero(val) }; + if (signEqual) { + if (abs(val) > abs(max)) { + max = val; + } + } else if (abs(val) <= abs(thrhold)) { + //if (i >= 553180) { + val = (max + val) / 2; + signEqual = lessThanZero(v) == lessThanZero(val); + //} + } + if (!signEqual) { + break; + } + ++i; + //}); + } + if (i < size) { + max = ch.at(i); + ++i; + } +// uint64_t d = std::distance(it, ch.end()); +// qDebug() << "Distance: " << d; + } + +} + void WavReader::normalizeWaveform(uint chNum) { if (chNum >= mWavFormatHeader.numberOfChannels) { diff --git a/sources/core/wavreader.h b/sources/core/wavreader.h index 1e4dbbc..11f9ae4 100644 --- a/sources/core/wavreader.h +++ b/sources/core/wavreader.h @@ -17,6 +17,7 @@ #include #include #include +#include #include "sources/defines.h" class WavReader : public QObject @@ -79,7 +80,7 @@ class WavReader : public QObject template const T* readData(QByteArray& buf) { buf = mWavFile.read(sizeof(T)); - if (buf.size() < sizeof(T)) { + if ((unsigned) buf.size() < sizeof(T)) { return nullptr; } return reinterpret_cast(buf.data()); @@ -109,13 +110,14 @@ class WavReader : public QObject QWavVectorType getSample(QByteArray& buf, size_t& bufIndex, uint dataSize, uint compressionCode) const; QWavVector* createVector(size_t bytesPerSample, size_t size); + unsigned calculateOnesInByte(uint8_t n); WavFmt mWavFormatHeader; WavChunk mCurrentChunk; bool mWavOpened; QFile mWavFile; - QScopedPointer mChannel0; - QScopedPointer mChannel1; + QSharedPointer mChannel0; + QSharedPointer mChannel1; QMap> mStoredChannels; protected: @@ -139,19 +141,21 @@ class WavReader : public QObject uint getNumberOfChannels() const; uint32_t getSampleRate() const; uint getBytesPerSample() const; - QWavVector* /*const*/ getChannel0() const; - QWavVector* /*const*/ getChannel1() const; + QSharedPointer getChannel0() const; + QSharedPointer getChannel1() const; ErrorCodesEnum setFileName(const QString& fileName); ErrorCodesEnum open(); ErrorCodesEnum read(); ErrorCodesEnum close(); + void loadTap(const QString& fname); void loadWaveform(const QString& fname); void saveWaveform(const QString& fname = QString()) const; void shiftWaveform(uint chNum); void storeWaveform(uint chNum); void restoreWaveform(uint chNum); + void repairWaveform(uint chNum); void normalizeWaveform(uint chNum); void normalizeWaveform2(uint chNum); diff --git a/sources/defines.h b/sources/defines.h index 3d4a311..cd4b589 100644 --- a/sources/defines.h +++ b/sources/defines.h @@ -30,18 +30,25 @@ inline bool isFreqFitsInDelta(uint32_t sampleRate, uint32_t length, uint32_t sig return freq >= (signalFreq - delta) && freq <= (signalFreq + delta); }; +inline bool isFreqFitsInDelta2(uint32_t sampleRate, uint32_t length, uint32_t signalFreq, double signalDeltaBelow, double signalDeltaAbove) { + const double freq = sampleRate / length; + const double deltaB = signalFreq * signalDeltaBelow; + const double deltaA = signalFreq * signalDeltaAbove; + return freq >= (signalFreq - deltaB) && freq <= (signalFreq + deltaA); +}; + enum SignalFrequencies { - PILOT_HALF_FREQ = 1620, - PILOT_FREQ = 810, - SYNCHRO_FIRST_HALF_FREQ = 4900, + PILOT_HALF_FREQ = 1660, + PILOT_FREQ = 830, + SYNCHRO_FIRST_HALF_FREQ = 6300, SYNCHRO_SECOND_HALF_FREQ = 5500, - SYNCHRO_FREQ = 2600, - ZERO_HALF_FREQ = 4090, + SYNCHRO_FREQ = 2950, + ZERO_HALF_FREQ = 4200, ZERO_FIRST_HALF_FREQ = 0, ZERO_SECOND_HALF_FREQ = 0, - ZERO_FREQ = 2050, - ONE_HALF_FREQ = 2045, - ONE_FREQ = 1023 + ZERO_FREQ = 2100, + ONE_HALF_FREQ = 2100, + ONE_FREQ = 1050 }; const bool checkForAbnormalSine = true; @@ -50,5 +57,6 @@ const double pilotDelta = 0.1; const double synchroDelta = 0.3; const double zeroDelta = 0.3;//0.3;//0.18; const double oneDelta = 0.25;//0.25;//0.1; +const double sineCheckTolerance = 0.5; #endif // DEFINES_H diff --git a/sources/main.cpp b/sources/main.cpp index 9286b9d..9c1fc14 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -18,6 +18,8 @@ #include "sources/models/fileworkermodel.h" #include "sources/models/suspiciouspointsmodel.h" #include "sources/models/parsersettingsmodel.h" +#include "sources/models/actionsmodel.h" +#include "sources/models/dataplayermodel.h" #include "sources/translations/translationmanager.h" void registerTypes() @@ -37,7 +39,9 @@ void registerTypes() qmlRegisterSingletonInstance("com.core.zxtapereviver", 1, 0, "WavReader", WavReader::instance()); qmlRegisterSingletonInstance("com.core.zxtapereviver", 1, 0, "WaveformParser", WaveformParser::instance()); qmlRegisterSingletonInstance("com.models.zxtapereviver", 1, 0, "ParserSettingsModel", ParserSettingsModel::instance()); + qmlRegisterSingletonInstance("com.models.zxtapereviver", 1, 0, "ActionsModel", ActionsModel::instance()); qmlRegisterSingletonInstance("com.models.zxtapereviver", 1, 0, "ConfigurationManager", ConfigurationManager::instance()); + qmlRegisterSingletonInstance("com.models.zxtapereviver", 1, 0, "DataPlayerModel", DataPlayerModel::instance()); } int main(int argc, char *argv[]) diff --git a/sources/models/actionsmodel.cpp b/sources/models/actionsmodel.cpp new file mode 100644 index 0000000..478b84b --- /dev/null +++ b/sources/models/actionsmodel.cpp @@ -0,0 +1,54 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + +#include "actionsmodel.h" +#include +#include "sources/actions/shiftwaveformaction.h" + +ActionsModel::ActionsModel(QObject* parent) : + QObject(parent) +{ + +} + +void ActionsModel::addAction(QSharedPointer action) { + if (action->apply()) { + m_actions.append(action); + emit actionsChanged(); + } +} + +void ActionsModel::removeAction() { + if (!m_actions.isEmpty()) { + m_actions.takeLast()->undo(); + emit actionsChanged(); + } +} + +void ActionsModel::shiftWaveform(double offset) { + addAction(QSharedPointer::create(0, ShiftWaveFormActionParams { static_cast(offset) })); +} + + +QVariantList ActionsModel::getActions() const { + QVariantList result; + for (const auto& a: m_actions) { + result.append(QVariantMap { { "name", a->actionName() } }); + } + return result; +} + +ActionsModel* ActionsModel::instance() { + static ActionsModel m; + return &m; +} diff --git a/sources/models/actionsmodel.h b/sources/models/actionsmodel.h new file mode 100644 index 0000000..ec13a63 --- /dev/null +++ b/sources/models/actionsmodel.h @@ -0,0 +1,54 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + +#ifndef ACTIONSMODEL_H +#define ACTIONSMODEL_H + +#include +#include +#include +#include +#include "sources/actions/actionbase.h" + +class ActionsModel final : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QVariantList actions READ getActions NOTIFY actionsChanged) + + QList> m_actions; + +protected: + explicit ActionsModel(QObject* parent = nullptr); + +public: + virtual ~ActionsModel() = default; + + ActionsModel(const ActionsModel& other) = delete; + ActionsModel(ActionsModel&& other) = delete; + ActionsModel& operator= (const ActionsModel& other) = delete; + ActionsModel& operator= (ActionsModel&& other) = delete; + + static ActionsModel* instance(); + + QVariantList getActions() const; + + void addAction(QSharedPointer action); + Q_INVOKABLE void removeAction(); + Q_INVOKABLE void shiftWaveform(double offset); + +signals: + void actionsChanged(); +}; + +#endif // ACTIONSMODEL_H diff --git a/sources/models/dataplayermodel.cpp b/sources/models/dataplayermodel.cpp new file mode 100644 index 0000000..fa3f9e9 --- /dev/null +++ b/sources/models/dataplayermodel.cpp @@ -0,0 +1,224 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + +#include "dataplayermodel.h" +#include + +DataPlayerModel::DataPlayerModel(QObject* parent) : + QObject(parent), + m_playingState(DP_Stopped), + m_blockTime(0), + m_processedTime(0) +{ + connect(&m_delayTimer, &QTimer::timeout, this, &DataPlayerModel::handleNextDataRecord); +} + +void DataPlayerModel::playParsedData(uint chNum, uint currentBlock) { + if (m_playingState != DP_Stopped) { + return; + } + +#if (Q_BYTE_ORDER == Q_BIG_ENDIAN) + const auto endianness { QAudioFormat::BigEndian }; +#else + const auto endianness { QAudioFormat::LittleEndian }; +#endif + + QAudioFormat format; + // Set up the format + format.setSampleRate(c_sampleRate); + format.setChannelCount(1); + format.setSampleSize(16); + format.setCodec("audio/pcm"); + format.setByteOrder(endianness); + format.setSampleType(QAudioFormat::SignedInt); + + const QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice()); + if (!info.isFormatSupported(format)) { + qDebug() << "Audio format not supported, cannot play audio."; + return; + } + + m_audio.reset(new QAudioOutput(info, format)); + m_audio->setNotifyInterval(30); + connect(m_audio.data(), &QAudioOutput::stateChanged, this, &DataPlayerModel::handleAudioOutputStateChanged); + connect(m_audio.data(), &QAudioOutput::notify, this, &DataPlayerModel::handleAudioOutputNotify); + + m_buffer.close(); + m_currentBlock = currentBlock; + m_data = WaveformParser::instance()->getParsedData(chNum); + m_parserData = chNum == 0 ? WaveformParser::instance()->getParsedChannel0() : WaveformParser::instance()->getParsedChannel1(); + handleNextDataRecord(); +} + +void DataPlayerModel::handleNextDataRecord() { + if (m_currentBlock >= (unsigned) m_data.first.size()) { + return; + } + + QByteArray array; + + const auto c_pilotHalfFreq { SignalFrequencies::PILOT_HALF_FREQ }; + const auto c_synchroFirstHalfFreq { SignalFrequencies::SYNCHRO_FIRST_HALF_FREQ }; + const auto c_synchroSecondHalfFreq { SignalFrequencies::SYNCHRO_SECOND_HALF_FREQ }; + const auto c_zeroHalfFreq { SignalFrequencies::ZERO_HALF_FREQ }; + const auto c_oneHalfFreq { SignalFrequencies::ONE_HALF_FREQ }; + + const auto oneFreq { c_oneHalfFreq / 2 }; + const auto oneHalfFreq { oneFreq * 2 }; + const auto zeroFreq { c_zeroHalfFreq / 2 }; + const auto zeroHalfFreq { zeroFreq * 2 }; + const auto synchroFirstHalf { c_synchroFirstHalfFreq }; + const auto synchroSecondHalf { c_synchroSecondHalfFreq }; + const auto pilotFreq { c_pilotHalfFreq / 2 }; + const auto pilotHalfFreq { pilotFreq * 2 }; + const auto pilotLen { 3 }; + + //Pilot + size_t wavlen = c_sampleRate / pilotHalfFreq; + auto threshold { c_sampleRate * pilotLen / (wavlen * 2) }; + for (size_t i { 0 }; i < threshold; ++i) { + for (auto p { 0 }; p <= 1; ++p) { + const int16_t val { int16_t(32760 * (p ? 1 : -1)) }; + for (size_t c { 0 }; c < wavlen; ++c) { + array.append((char *)&val, sizeof(int16_t)); + } + } + } + //Synchro + for (auto w { 0 }; w <= 1; ++w) { + wavlen = c_sampleRate / (w ? synchroSecondHalf : synchroFirstHalf); + const int16_t val { int16_t(32760 * (w ? 1 : -1)) }; + for (size_t c { 0 }; c < wavlen; ++c) { + array.append((char *)&val, sizeof(int16_t)); + } + } + //Data + for (const uint8_t byte: qAsConst(m_data.first[m_currentBlock].data)) { + for (int i { 7 }; i >= 0; --i) { + const uint8_t bit8 = 1 << i; + const auto bit { byte & bit8 }; + wavlen = c_sampleRate / (bit == 0 ? zeroHalfFreq : oneHalfFreq); + for (auto b { 0 }; b <= 1; ++b) { + const int16_t val { int16_t(32760 * (b ? 1 : -1)) }; + for (size_t c { 0 }; c < wavlen; ++c) { + array.append((char *)&val, sizeof(int16_t)); + } + } + } + } + + emit currentBlockChanged(); + m_blockTime = (array.size() / sizeof(int16_t)) / (c_sampleRate / 1000); + m_processedTime = 0; + emit blockTimeChanged(); + emit processedTimeChanged(); + ++m_currentBlock; + + m_buffer.setData(array); + m_buffer.open(QIODevice::ReadOnly); + m_audio->start(&m_buffer); +} + +void DataPlayerModel::prepareNextDataRecord() { + const QVector& blockData { m_data.first }; + const QVector& selectionData { m_data.second }; + while (m_currentBlock < (unsigned) blockData.size()) { + if ((unsigned) selectionData.size() < m_currentBlock || selectionData.at(m_currentBlock)) { + break; + } + ++m_currentBlock; + } + + m_buffer.close(); + if (m_currentBlock < (unsigned) blockData.size()) { + //half a second delay + m_delayTimer.singleShot(500, this, [this]() { + //We have to check for state == playing to be sure stop method is not executed previously. + if (m_playingState == DP_Playing) { + handleNextDataRecord(); + } + }); + } else { + m_audio->stop(); + m_audio.reset(); + m_playingState = DP_Stopped; + emit currentBlockChanged(); + emit stoppedChanged(); + } +} + +void DataPlayerModel::handleAudioOutputStateChanged(QAudio::State state) { + switch (state) { + case QAudio::IdleState: + prepareNextDataRecord(); + break; + + case QAudio::StoppedState: + if (m_audio->error() != QAudio::NoError) { + qDebug() << "Error playing: " << m_audio->error(); + } + break; + + case QAudio::ActiveState: + m_playingState = DP_Playing; + emit stoppedChanged(); + break; + + default: + break; + } +} + +void DataPlayerModel::stop() { + if (m_playingState != DP_Stopped) { + m_currentBlock = m_data.first.size(); + prepareNextDataRecord(); + } +} + +void DataPlayerModel::handleAudioOutputNotify() { + m_processedTime = m_audio->processedUSecs() / 1000; + emit processedTimeChanged(); +} + +bool DataPlayerModel::getStopped() const { + return m_playingState == DP_Stopped; +} + +int DataPlayerModel::getCurrentBlock() const { + return m_currentBlock < (unsigned) m_data.first.size() ? m_currentBlock : -1; +} + +int DataPlayerModel::getBlockTime() const { + return m_blockTime; +} + +int DataPlayerModel::getProcessedTime() const { + return m_processedTime; +} + +QVariant DataPlayerModel::getBlockData() const { + const auto cb { getCurrentBlock() }; + return cb < 0 ? QVariant() : m_parserData.at(cb); +} + +DataPlayerModel::~DataPlayerModel() { + m_audio.reset(); + qDebug() << "~DataPlayerModel"; +} + +DataPlayerModel* DataPlayerModel::instance() { + static DataPlayerModel m; + return &m; +} diff --git a/sources/models/dataplayermodel.h b/sources/models/dataplayermodel.h new file mode 100644 index 0000000..e87eacb --- /dev/null +++ b/sources/models/dataplayermodel.h @@ -0,0 +1,81 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + +#ifndef DATAPLAYERMODEL_H +#define DATAPLAYERMODEL_H + +#include +#include +#include +#include "sources/core/waveformparser.h" + +class DataPlayerModel : public QObject +{ + Q_OBJECT + + enum PlayingState { + DP_Stopped = 0, + DP_Playing, + DP_Paused + }; + + Q_PROPERTY(bool stopped READ getStopped NOTIFY stoppedChanged) + Q_PROPERTY(int currentBlock READ getCurrentBlock NOTIFY currentBlockChanged) + Q_PROPERTY(int blockTime READ getBlockTime NOTIFY blockTimeChanged) + Q_PROPERTY(int processedTime READ getProcessedTime NOTIFY processedTimeChanged) + Q_PROPERTY(QVariant blockData READ getBlockData NOTIFY currentBlockChanged) + + PlayingState m_playingState; + QScopedPointer m_audio; + QPair, QVector> m_data; + QVariantList m_parserData; + unsigned m_currentBlock; + QTimer m_delayTimer; + QBuffer m_buffer; + const unsigned c_sampleRate { 44100 }; + int m_blockTime; + int m_processedTime; + +protected slots: + void handleAudioOutputStateChanged(QAudio::State state); + void handleAudioOutputNotify(); + void handleNextDataRecord(); + +protected: + explicit DataPlayerModel(QObject* parent = nullptr); + void prepareNextDataRecord(); + +public: + virtual ~DataPlayerModel() override; + + bool getStopped() const; + int getCurrentBlock() const; + int getBlockTime() const; + int getProcessedTime() const; + QVariant getBlockData() const; + + Q_INVOKABLE void playParsedData(uint chNum, uint currentBlock = 0); + Q_INVOKABLE void stop(); + //Q_INVOKABLE void pause(); + //Q_INVOKABLE void resume(); + + static DataPlayerModel* instance(); + +signals: + void stoppedChanged(); + void currentBlockChanged(); + void blockTimeChanged(); + void processedTimeChanged(); +}; + +#endif // DATAPLAYERMODEL_H diff --git a/sources/models/fileworkermodel.cpp b/sources/models/fileworkermodel.cpp index 0c6b1c9..a3668b7 100644 --- a/sources/models/fileworkermodel.cpp +++ b/sources/models/fileworkermodel.cpp @@ -23,6 +23,21 @@ FileWorkerModel::FileWorkerModel(QObject* parent) : } +/*WavReader::ErrorCodesEnum*/ int FileWorkerModel::openTapFileByUrl(const QString& fileNameUrl) { + QUrl u(fileNameUrl); + return openTapFile(u.toLocalFile()); +} + +/*WavReader::ErrorCodesEnum*/ int FileWorkerModel::openTapFile(const QString& fileName) { + auto& r = *WavReader::instance(); + r.close(); + + r.loadTap(fileName); + m_wavFileName = fileName; + emit wavFileNameChanged(); + return WavReader::Ok; +} + /*WavReader::ErrorCodesEnum*/ int FileWorkerModel::openWavFileByUrl(const QString& fileNameUrl) { QUrl u(fileNameUrl); diff --git a/sources/models/fileworkermodel.h b/sources/models/fileworkermodel.h index 4b4d7fe..18fb0a5 100644 --- a/sources/models/fileworkermodel.h +++ b/sources/models/fileworkermodel.h @@ -31,13 +31,15 @@ class FileWorkerModel : public QObject Q_ENUM(FileWorkerResults) explicit FileWorkerModel(QObject* parent = nullptr); - ~FileWorkerModel(); + virtual ~FileWorkerModel() override; //getters QString getWavFileName() const; //setters //QML invokable members + Q_INVOKABLE /*WavReader::ErrorCodesEnum*/ int openTapFileByUrl(const QString& fileNameUrl); + Q_INVOKABLE /*WavReader::ErrorCodesEnum*/ int openTapFile(const QString& fileName); Q_INVOKABLE /*WavReader::ErrorCodesEnum*/ int openWavFileByUrl(const QString& fileNameUrl); Q_INVOKABLE /*WavReader::ErrorCodesEnum*/ int openWavFile(const QString& fileName); Q_INVOKABLE /*WavReader::ErrorCodesEnum*/ int openWaveformFileByUrl(const QString& fileNameUrl); diff --git a/sources/models/parsersettingsmodel.cpp b/sources/models/parsersettingsmodel.cpp index 71a583f..608c3ad 100644 --- a/sources/models/parsersettingsmodel.cpp +++ b/sources/models/parsersettingsmodel.cpp @@ -31,7 +31,8 @@ ParserSettingsModel::ParserSettingsModel(QObject* parent) : synchroDelta, zeroDelta, oneDelta, - checkForAbnormalSine } + checkForAbnormalSine, + sineCheckTolerance } { } @@ -52,6 +53,7 @@ void ParserSettingsModel::restoreDefaultSettings() setZeroDelta(zeroDelta); setOneDelta(oneDelta); setCheckForAbnormalSine(checkForAbnormalSine); + setSineCheckTolerance(sineCheckTolerance); } const ParserSettingsModel::ParserSettings& ParserSettingsModel::getParserSettings() const @@ -129,6 +131,10 @@ bool ParserSettingsModel::getCheckForAbnormalSine() const return m_parserSettings.checkForAbnormalSine; } +double ParserSettingsModel::getSineCheckTolerance() const { + return m_parserSettings.sineCheckTolerance; +} + void ParserSettingsModel::setPilotHalfFreq(int freq) { if (m_parserSettings.pilotHalfFreq != freq) { @@ -241,6 +247,13 @@ void ParserSettingsModel::setCheckForAbnormalSine(bool check) } } +void ParserSettingsModel::setSineCheckTolerance(double value) { + if (m_parserSettings.sineCheckTolerance != value) { + m_parserSettings.sineCheckTolerance = value; + emit sineCheckToleranceChanged(); + } +} + ParserSettingsModel* ParserSettingsModel::instance() { static QScopedPointer m { new ParserSettingsModel() }; diff --git a/sources/models/parsersettingsmodel.h b/sources/models/parsersettingsmodel.h index 74492d4..6393633 100644 --- a/sources/models/parsersettingsmodel.h +++ b/sources/models/parsersettingsmodel.h @@ -34,6 +34,10 @@ class ParserSettingsModel : public QObject Q_PROPERTY(double zeroDelta READ getZeroDelta WRITE setZeroDelta NOTIFY zeroDeltaChanged) Q_PROPERTY(double oneDelta READ getOneDelta WRITE setOneDelta NOTIFY oneDeltaChanged) Q_PROPERTY(bool checkForAbnormalSine READ getCheckForAbnormalSine WRITE setCheckForAbnormalSine NOTIFY checkForAbnormalSineChanged) + Q_PROPERTY(double sineCheckTolerance READ getSineCheckTolerance WRITE setSineCheckTolerance NOTIFY sineCheckToleranceChanged) + +protected: + explicit ParserSettingsModel(QObject* parent = nullptr); public: struct ParserSettings { @@ -51,9 +55,9 @@ class ParserSettingsModel : public QObject double zeroDelta; double oneDelta; bool checkForAbnormalSine; + double sineCheckTolerance; }; - explicit ParserSettingsModel(QObject* parent = nullptr); virtual ~ParserSettingsModel() = default; const ParserSettings& getParserSettings() const; @@ -76,6 +80,7 @@ class ParserSettingsModel : public QObject double getZeroDelta() const; double getOneDelta() const; bool getCheckForAbnormalSine() const; + double getSineCheckTolerance() const; //Setters void setPilotHalfFreq(int freq); @@ -92,6 +97,7 @@ class ParserSettingsModel : public QObject void setZeroDelta(double delta); void setOneDelta(double delta); void setCheckForAbnormalSine(bool check); + void setSineCheckTolerance(double value); signals: void pilotHalfFreqChanged(); @@ -108,6 +114,7 @@ class ParserSettingsModel : public QObject void zeroDeltaChanged(); void oneDeltaChanged(); void checkForAbnormalSineChanged(); + void sineCheckToleranceChanged(); private: ParserSettings m_parserSettings; diff --git a/sources/models/waveformmodel.cpp b/sources/models/waveformmodel.cpp new file mode 100644 index 0000000..818d090 --- /dev/null +++ b/sources/models/waveformmodel.cpp @@ -0,0 +1,32 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + +#include "waveformmodel.h" + +WaveFormModel::WaveFormModel() +{ + +} + +void WaveFormModel::initialize(QPair, QSharedPointer> channels) { + m_channels = QVector>({ channels.first, channels.second }); +} + +QSharedPointer WaveFormModel::getChannel(int channel) { + return channel < m_channels.size() ? m_channels.at(channel) : QSharedPointer::create(); +} + +WaveFormModel* WaveFormModel::instance() { + static WaveFormModel m; + return &m; +} diff --git a/sources/models/waveformmodel.h b/sources/models/waveformmodel.h new file mode 100644 index 0000000..b6ef525 --- /dev/null +++ b/sources/models/waveformmodel.h @@ -0,0 +1,42 @@ +//******************************************************************************* +// 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. +//******************************************************************************* + +#ifndef WAVEFORMMODEL_H +#define WAVEFORMMODEL_H + +#include "sources/defines.h" +#include +#include + +class WaveFormModel final +{ + QVector> m_channels; + +private: + WaveFormModel(); + +public: + ~WaveFormModel() = default; + + WaveFormModel(const WaveFormModel& other) = delete; + WaveFormModel(WaveFormModel&& other) = delete; + WaveFormModel& operator= (const WaveFormModel& other) = delete; + WaveFormModel& operator= (WaveFormModel&& other) = delete; + + static WaveFormModel* instance(); + + void initialize(QPair, QSharedPointer> channels); + QSharedPointer getChannel(int channel); +}; + +#endif // WAVEFORMMODEL_H diff --git a/sources/translations/translations.cpp b/sources/translations/translations.cpp index c7bd7f3..c85a31e 100644 --- a/sources/translations/translations.cpp +++ b/sources/translations/translations.cpp @@ -16,9 +16,12 @@ #include TRANSLATION_IDS_CODE -const char* ID_TIMELINE_SEC = QT_TRID_NOOP("id_timeline_sec"); -const char* ID_OK = QT_TRID_NOOP("id_ok"); -const char* ID_ERROR = QT_TRID_NOOP("id_error"); -const char* ID_UNKNOWN = QT_TRID_NOOP("id_unknown"); -const char* ID_HEADER = QT_TRID_NOOP("id_header"); -const char* ID_CODE = QT_TRID_NOOP("id_code"); +const char* ID_TIMELINE_SEC = QT_TRID_NOOP("id_timeline_sec"); +const char* ID_OK = QT_TRID_NOOP("id_ok"); +const char* ID_ERROR = QT_TRID_NOOP("id_error"); +const char* ID_UNKNOWN = QT_TRID_NOOP("id_unknown"); +const char* ID_HEADER = QT_TRID_NOOP("id_header"); +const char* ID_CODE = QT_TRID_NOOP("id_code"); +const char* ID_EDIT_ACTION = QT_TRID_NOOP("id_edit_action"); +const char* ID_SHIFT_WAVEFORM_ACTION = QT_TRID_NOOP("id_shift_waveform_action"); +const char* ID_PARITY_MESSAGE = QT_TRID_NOOP("id_parity_message"); diff --git a/sources/translations/translations.h b/sources/translations/translations.h index 2500834..884d72f 100644 --- a/sources/translations/translations.h +++ b/sources/translations/translations.h @@ -22,5 +22,8 @@ extern const char* ID_ERROR; extern const char* ID_UNKNOWN; extern const char* ID_HEADER; extern const char* ID_CODE; +extern const char* ID_EDIT_ACTION; +extern const char* ID_SHIFT_WAVEFORM_ACTION; +extern const char* ID_PARITY_MESSAGE; #endif // TRANSLATIONS_H