diff --git a/ZXTapeReviver.pro b/ZXTapeReviver.pro
new file mode 100644
index 0000000..186da26
--- /dev/null
+++ b/ZXTapeReviver.pro
@@ -0,0 +1,42 @@
+QT += quick gui quickcontrols2
+
+CONFIG += c++11
+
+# The following define makes your compiler emit warnings if you use
+# any Qt feature that has been marked deprecated (the exact warnings
+# depend on your compiler). Refer to the documentation for the
+# deprecated API to know how to port your code away from it.
+DEFINES += QT_DEPRECATED_WARNINGS
+
+# You can also make your code fail to compile if it uses deprecated APIs.
+# In order to do so, uncomment the following line.
+# You can also select to disable deprecated APIs only up to a certain version of Qt.
+#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
+CONFIG += c++14
+
+SOURCES += \
+ sources/main.cpp \
+ sources/models/fileworkermodel.cpp \
+ sources/controls/waveformcontrol.cpp \
+ sources/core/waveformparser.cpp \
+ sources/core/wavreader.cpp
+
+HEADERS += \
+ sources/defines.h \
+ sources/models/fileworkermodel.h \
+ sources/controls/waveformcontrol.h \
+ sources/core/waveformparser.h \
+ sources/core/wavreader.h
+
+RESOURCES += qml/qml.qrc
+
+# Additional import path used to resolve QML modules in Qt Creator's code model
+QML_IMPORT_PATH =
+
+# Additional import path used to resolve QML modules just for Qt Quick Designer
+QML_DESIGNER_IMPORT_PATH =
+
+# Default rules for deployment.
+qnx: target.path = /tmp/$${TARGET}/bin
+else: unix:!android: target.path = /opt/$${TARGET}/bin
+!isEmpty(target.path): INSTALLS += target
diff --git a/qml/main.qml b/qml/main.qml
new file mode 100644
index 0000000..c349c40
--- /dev/null
+++ b/qml/main.qml
@@ -0,0 +1,683 @@
+//*******************************************************************************
+// ZX Tape Reviver
+//-----------------
+//
+// Author: Leonid Golouz
+// E-mail: lgolouz@list.ru
+//*******************************************************************************
+
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import QtQuick.Controls 1.4
+import QtQuick.Dialogs 1.3
+
+import WaveformControl 1.0
+import com.enums.zxtapereviver 1.0
+import com.models.zxtapereviver 1.0
+import com.core.zxtapereviver 1.0
+
+ApplicationWindow {
+ id: mainWindow
+
+ readonly property int mainAreaWidth: width * 0.75
+ property ListModel suspiciousPoints: ListModel { }
+
+ visible: true
+ width: 1600
+ height: 800
+ title: qsTr("ZX Tape Reviver")
+
+ function getWaveShiftIndex(wfWidth, wfXScale) {
+ return wfWidth * wfXScale / 2;
+ }
+
+ function addSuspiciousPoint(idx) {
+ console.log("Adding suspicious point: " + idx);
+ for (var i = 0; i < suspiciousPoints.count; ++i) {
+ var p = suspiciousPoints.get(i).idx;
+ if (p === idx) {
+ return;
+ }
+ else if (p > idx) {
+ suspiciousPoints.insert(i, {idx: idx});
+ return;
+ }
+ }
+
+ suspiciousPoints.append({idx: idx});
+ }
+
+ function getSelectedWaveform() {
+ return channelsComboBox.currentIndex == 0 ? waveformControlCh0 : waveformControlCh1;
+ }
+
+ menuBar: MenuBar {
+ Menu {
+ title: "File"
+
+ MenuItem {
+ text: "Open WAV file..."
+ onTriggered: {
+ console.log("Opening WAV file");
+ openFileDialog.open();
+ }
+ }
+
+ MenuSeparator { }
+
+ Menu {
+ title: "Save"
+
+ Menu {
+ title: "Parsed"
+
+ MenuItem {
+ text: "Left channel..."
+
+ onTriggered: {
+ saveFileDialog.saveParsed = true;
+ saveFileDialog.channelNumber = 0;
+ saveFileDialog.open();
+ }
+ }
+
+ MenuItem {
+ text: "Right channel..."
+
+ onTriggered: {
+ saveFileDialog.saveParsed = true;
+ saveFileDialog.channelNumber = 1;
+ saveFileDialog.open();
+ }
+ }
+ }
+
+ MenuItem {
+ text: "Waveform..."
+
+ onTriggered: {
+ saveFileDialog.saveParsed = false;
+ saveFileDialog.open();
+ }
+ }
+ }
+
+ MenuSeparator { }
+
+ MenuItem {
+ text: "Exit"
+ onTriggered: {
+ mainWindow.close();
+ }
+ }
+ }
+
+ Menu {
+ title: "Waveform"
+
+ MenuItem {
+ text: "Restore"
+ onTriggered: {
+ waveformControlCh0.xScaleFactor = 1;
+ waveformControlCh0.yScaleFactor = 80000;
+ waveformControlCh0.wavePos = 0;
+
+ waveformControlCh1.xScaleFactor = 1;
+ waveformControlCh1.yScaleFactor = 80000;
+ waveformControlCh1.wavePos = 0;
+ }
+ }
+
+ MenuItem {
+ text: "Reparse"
+ }
+ }
+ }
+
+ FileDialog {
+ id: openFileDialog
+
+ title: "Please choose WAV file"
+ selectMultiple: false
+ sidebarVisible: true
+ defaultSuffix: "wav"
+ nameFilters: [ "WAV files (*.wav)" ]
+
+ onAccepted: {
+ console.log("Selected WAV file: " + openFileDialog.fileUrl);
+ console.log("Open WAV file result: " + FileWorkerModel.openWavFileByUrl(openFileDialog.fileUrl));
+ }
+
+ onRejected: {
+ console.log("No WAV file selected");
+ }
+ }
+
+ FileDialog {
+ id: saveFileDialog
+
+ property bool saveParsed: true
+ property int channelNumber: 0
+
+ title: saveParsed ? "Save TAP file..." : "Save WFM file..."
+ selectExisting: false
+ selectMultiple: false
+ sidebarVisible: true
+ defaultSuffix: saveParsed ? "tap" : "wfm"
+ nameFilters: saveParsed ? [ "TAP tape files (*.tap)" ] : [ "WFM waveform files (*.wfm)" ]
+
+ onAccepted: {
+ if (saveParsed) {
+ if (channelNumber == 0) {
+ waveformControlCh0.saveTap(saveFileDialog.fileUrl);
+ }
+ else {
+ waveformControlCh1.saveTap(saveFileDialog.fileUrl);
+ }
+ }
+ else {
+ //wavReader.saveWaveform();
+ }
+ }
+ }
+
+ Connections {
+ target: FileWorkerModel
+ function onWavFileNameChanged() {
+ waveformControlCh0.reparse();
+ waveformControlCh1.reparse();
+ }
+ }
+
+ Rectangle {
+ id: mainArea
+
+ anchors {
+ left: parent.left
+ top: parent.top
+ bottom: parent.bottom
+ }
+ width: mainAreaWidth
+ color: "black"
+
+ readonly property int spacerHeight: ~~(parent.height * 0.0075);
+
+ WaveformControl {
+ id: waveformControlCh0
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: vZoomInButton.left
+ anchors.rightMargin: 5
+
+ channelNumber: 0
+ width: parent.width - (parent.width * 0.11)
+ height: parent.height - parent.height / 2 - parent.spacerHeight / 2
+
+ onDoubleClick: {
+ addSuspiciousPoint(idx);
+ }
+ }
+
+ WaveformControl {
+ id: waveformControlCh1
+
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: vZoomInButton.left
+ anchors.rightMargin: 5
+
+ channelNumber: 1
+ width: parent.width - (parent.width * 0.11)
+ height: waveformControlCh0.height
+
+ onDoubleClick: {
+ addSuspiciousPoint(idx);
+ }
+ }
+
+ Button {
+ id: vZoomInButton
+
+ text: "Vertical Zoom IN"
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.rightMargin: 5
+ anchors.topMargin: 5
+ width: hZoomOutButton.width
+
+ onClicked: {
+ var yfactor = waveformControlCh0.yScaleFactor;
+ if (yfactor > 1000) {
+ waveformControlCh0.yScaleFactor = yfactor / 2;
+ }
+
+ yfactor = waveformControlCh1.yScaleFactor;
+ if (yfactor > 1000) {
+ waveformControlCh1.yScaleFactor = yfactor / 2;
+ }
+ }
+ }
+
+ Button {
+ id: vZoomOutButton
+
+ text: "Vertical Zoom OUT"
+ anchors.top: vZoomInButton.bottom
+ anchors.right: parent.right
+ anchors.rightMargin: 5
+ anchors.topMargin: 5
+ width: hZoomOutButton.width
+
+ onClicked: {
+ var yfactor = waveformControlCh0.yScaleFactor;
+ if (yfactor < 320000) {
+ waveformControlCh0.yScaleFactor = yfactor * 2;
+ }
+
+ yfactor = waveformControlCh1.yScaleFactor;
+ if (yfactor < 320000) {
+ waveformControlCh1.yScaleFactor = yfactor * 2;
+ }
+ }
+ }
+
+ Button {
+ id: hZoomInButton
+
+ text: "Horizontal Zoom IN"
+ anchors.top: vZoomOutButton.bottom
+ anchors.right: parent.right
+ anchors.rightMargin: 5
+ anchors.topMargin: 10
+ width: hZoomOutButton.width
+
+ onClicked: {
+ waveformControlCh0.xScaleFactor = waveformControlCh0.xScaleFactor / 2;
+ waveformControlCh1.xScaleFactor = waveformControlCh1.xScaleFactor / 2;
+ }
+ }
+
+ Button {
+ id: hZoomOutButton
+
+ text: "Horizontal Zoom OUT"
+ anchors.top: hZoomInButton.bottom
+ anchors.right: parent.right
+ anchors.rightMargin: 5
+ anchors.topMargin: 5
+
+ onClicked: {
+ waveformControlCh0.xScaleFactor = waveformControlCh0.xScaleFactor * 2;
+ waveformControlCh1.xScaleFactor = waveformControlCh1.xScaleFactor * 2;
+ }
+ }
+
+ Button {
+ id: shiftWaveRight
+
+ text: "<<"
+ anchors.bottom: waveformControlCh0.bottom
+ anchors.left: hZoomOutButton.left
+ width: 40
+
+ onClicked: {
+ waveformControlCh0.wavePos -= getWaveShiftIndex(waveformControlCh0.width, waveformControlCh0.xScaleFactor);
+ waveformControlCh1.wavePos -= getWaveShiftIndex(waveformControlCh1.width, waveformControlCh1.xScaleFactor);
+ }
+ }
+
+ Button {
+ id: shiftWaveLeft
+
+ text: ">>"
+ anchors.bottom: shiftWaveRight.bottom
+ anchors.right: parent.right
+ anchors.rightMargin: 5
+ width: 40
+
+ onClicked: {
+ waveformControlCh0.wavePos += getWaveShiftIndex(waveformControlCh0.width, waveformControlCh0.xScaleFactor);
+ waveformControlCh1.wavePos += getWaveShiftIndex(waveformControlCh1.width, waveformControlCh1.xScaleFactor);
+ }
+ }
+
+ Button {
+ id: reparseButton
+
+ text: "Reparse"
+ anchors.top: shiftWaveLeft.bottom
+ anchors.right: parent.right
+ anchors.rightMargin: 5
+ anchors.topMargin: 15
+ width: hZoomOutButton.width
+
+ onClicked: {
+ var parsedDataViewIdx = parsedDataView.currentRow;
+ waveformControlCh0.reparse();
+ waveformControlCh1.reparse();
+ parsedDataView.selection.select(parsedDataViewIdx);
+ parsedDataView.currentRow = parsedDataViewIdx;
+ }
+ }
+
+ Button {
+ id: saveParsedDataButton
+
+ text: "Save parsed"
+ anchors.top: reparseButton.bottom
+ anchors.right: parent.right
+ anchors.rightMargin: 5
+ anchors.topMargin: 15
+ width: hZoomOutButton.width
+
+ onClicked: {
+ waveformControlCh0.saveTap();
+ //waveformControlCh1.saveTap();
+ }
+ }
+
+ Button {
+ id: saveWaveformButton
+
+ text: "Save waveform"
+ anchors.top: saveParsedDataButton.bottom
+ anchors.right: parent.right
+ anchors.rightMargin: 5
+ anchors.topMargin: 15
+ width: hZoomOutButton.width
+
+ onClicked: {
+ waveformControlCh0.saveWaveform();
+ //waveformControlCh1.saveWaveform();
+ }
+ }
+
+ Button {
+ id: repairRestoreButton
+
+ text: "%1 waveform".arg(getSelectedWaveform().isWaveformRepaired ? "Restore" : "Repair")
+ anchors.top: saveParsedDataButton.bottom
+ anchors.right: parent.right
+ anchors.rightMargin: 5
+ anchors.topMargin: 15
+ width: hZoomOutButton.width
+
+ onClicked: {
+ if (getSelectedWaveform().isWaveformRepaired) {
+ getSelectedWaveform().restoreWaveform();
+ }
+ else {
+ getSelectedWaveform().repairWaveform();
+ }
+ }
+ }
+
+ Button {
+ id: shiftWaveform
+
+ text: "Shift waveform"
+ anchors {
+ top: repairRestoreButton.bottom
+ topMargin: 5
+ right: parent.right
+ rightMargin: 5
+ }
+ width: hZoomOutButton.width
+
+ onClicked: {
+ getSelectedWaveform().shiftWaveform();
+ }
+ }
+
+ Button {
+ id: selectionModeToggleButton
+
+ text: "Selection mode"
+ anchors.right: parent.right
+ anchors.bottom: waveformControlCh1.bottom
+ anchors.bottomMargin: 5
+ anchors.rightMargin: 5
+ width: hZoomOutButton.width
+ checkable: true
+
+ onCheckedChanged: {
+ waveformControlCh0.selectionMode = waveformControlCh1.selectionMode = checked;
+ }
+ }
+
+ Button {
+ id: copyFromRigthToLeftChannel
+
+ anchors.right: parent.right
+ anchors.bottom: selectionModeToggleButton.top
+ anchors.bottomMargin: 15
+ anchors.rightMargin: 5
+ width: hZoomOutButton.width
+
+ text: "Copy from R to L (▲)"
+ visible: selectionModeToggleButton.checked
+ onClicked: {
+ waveformControlCh1.copySelectedToAnotherChannel();
+ waveformControlCh0.update();
+ }
+ }
+
+ Button {
+ id: copyFromLeftToRightChannel
+
+ anchors.right: parent.right
+ anchors.bottom: copyFromRigthToLeftChannel.top
+ anchors.bottomMargin: 5
+ anchors.rightMargin: 5
+ width: hZoomOutButton.width
+
+ text: "Copy from L to R (▼)"
+ visible: selectionModeToggleButton.checked
+ onClicked: {
+ waveformControlCh0.copySelectedToAnotherChannel();
+ waveformControlCh1.update();
+ }
+ }
+ }
+
+ Rectangle {
+ id: rightArea
+
+ anchors {
+ left: mainArea.right
+ top: parent.top
+ bottom: parent.bottom
+ }
+
+ width: parent.width - mainAreaWidth
+
+ color: "transparent"
+
+ ComboBox {
+ id: channelsComboBox
+
+ model: ["Left Channel", "Right Channel"]
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ }
+ }
+
+ Button {
+ id: toBlockBeginningButton
+
+ text: "<< To the beginning of the block"
+ anchors {
+ top: channelsComboBox.bottom
+ left: parent.left
+ rightMargin: 2
+ topMargin: 2
+ bottomMargin: 2
+ }
+ width: parent.width / 2
+
+ onClicked: {
+ if (parsedDataView.currentRow !== -1) {
+ var idx = WaveformParser.getBlockDataStart(channelsComboBox.currentIndex, parsedDataView.currentRow) - getWaveShiftIndex(waveformControlCh0.width, waveformControlCh0.xScaleFactor);
+ if (idx < 0) {
+ idx = 0;
+ }
+
+ waveformControlCh0.wavePos = waveformControlCh1.wavePos = idx ;
+ }
+ }
+ }
+
+ Button {
+ id: toBlockEndButton
+
+ text: "To the end of the block >>"
+ anchors {
+ top: channelsComboBox.bottom
+ right: parent.right
+ left: toBlockBeginningButton.right
+ leftMargin: 2
+ topMargin: 2
+ bottomMargin: 2
+ }
+
+ onClicked: {
+ if (parsedDataView.currentRow !== -1) {
+ var idx = WaveformParser.getBlockDataEnd(channelsComboBox.currentIndex, parsedDataView.currentRow) - getWaveShiftIndex(waveformControlCh0.width, waveformControlCh0.xScaleFactor);
+ if (idx < 0) {
+ idx = 0;
+ }
+
+ waveformControlCh0.wavePos = waveformControlCh1.wavePos = idx;
+ }
+ }
+ }
+
+ TableView {
+ id: parsedDataView
+
+ height: parent.height * 0.4
+ anchors {
+ top: toBlockEndButton.bottom
+ left: parent.left
+ right: parent.right
+ topMargin: 2
+ }
+
+ TableViewColumn {
+ title: "#"
+ width: rightArea.width * 0.07
+ role: "blockNumber"
+ }
+
+ TableViewColumn {
+ title: "Type"
+ width: rightArea.width * 0.23
+ role: "blockType"
+ }
+
+ TableViewColumn {
+ title: "Name"
+ width: rightArea.width * 0.3
+ role: "blockName"
+ }
+
+ TableViewColumn {
+ title: "Size (to be read)"
+ width: rightArea.width * 0.25
+ role: "blockSize"
+ }
+
+ TableViewColumn {
+ title: "Status"
+ width: rightArea.width * 0.15
+ role: "blockStatus"
+ }
+
+ selectionMode: SelectionMode.SingleSelection
+ model: channelsComboBox.currentIndex === 0 ? WaveformParser.parsedChannel0 : WaveformParser.parsedChannel1
+ itemDelegate: Text {
+ text: styleData.value
+ color: modelData.state === 0 ? "black" : "red"
+ }
+ }
+
+ Button {
+ id: gotoPointButton
+
+ anchors {
+ top: parsedDataView.bottom
+ left: parent.left
+ rightMargin: 2
+ topMargin: 2
+ }
+ width: parent.width / 2
+ text: "Goto Suspicious point"
+
+ onClicked: {
+ if (suspiciousPointsView.currentRow < 0 || suspiciousPointsView.currentRow >= suspiciousPoints.count) {
+ return;
+ }
+
+ var idx = suspiciousPoints.get(suspiciousPointsView.currentRow).idx - getWaveShiftIndex(waveformControlCh0.width, waveformControlCh0.xScaleFactor);
+ if (idx < 0) {
+ idx = 0;
+ }
+ console.log("Go to point: " + idx);
+
+ waveformControlCh0.wavePos = waveformControlCh1.wavePos = idx;
+ }
+ }
+
+ Button {
+ id: removePointButton
+
+ anchors {
+ top: parsedDataView.bottom
+ left: gotoPointButton.right
+ right: parent.right
+ leftMargin: 2
+ topMargin: 2
+ }
+
+ text: "Remove Suspicious point"
+
+ onClicked: {
+ if (suspiciousPointsView.currentRow >= 0 && suspiciousPointsView.currentRow < suspiciousPoints.count) {
+ console.log("Removing suspicious point: " + suspiciousPoints.get(suspiciousPointsView.currentRow).idx);
+ suspiciousPoints.remove(suspiciousPointsView.currentRow);
+ }
+ }
+ }
+
+ TableView {
+ id: suspiciousPointsView
+
+ anchors {
+ top: gotoPointButton.bottom
+ bottom: parent.bottom
+ left: parent.left
+ right: parent.right
+ topMargin: 2
+ }
+
+ selectionMode: SelectionMode.SingleSelection
+ model: suspiciousPoints
+ itemDelegate: Text {
+ text: styleData.column === 0 ? styleData.row + 1 : styleData.value
+ }
+
+ TableViewColumn {
+ title: "#"
+ width: rightArea.width * 0.1
+ }
+
+ TableViewColumn {
+ title: "Position"
+ width: rightArea.width * 0.9
+ }
+ }
+ }
+}
diff --git a/qml/qml.qrc b/qml/qml.qrc
new file mode 100644
index 0000000..5f6483a
--- /dev/null
+++ b/qml/qml.qrc
@@ -0,0 +1,5 @@
+
+
+ main.qml
+
+
diff --git a/sources/controls/waveformcontrol.cpp b/sources/controls/waveformcontrol.cpp
new file mode 100644
index 0000000..7eebb4f
--- /dev/null
+++ b/sources/controls/waveformcontrol.cpp
@@ -0,0 +1,442 @@
+//*******************************************************************************
+// ZX Tape Reviver
+//-----------------
+//
+// Author: Leonid Golouz
+// E-mail: lgolouz@list.ru
+//*******************************************************************************
+
+#include "waveformcontrol.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+WaveformControl::WaveformControl(QQuickItem* parent) :
+ QQuickPaintedItem(parent),
+ mWavReader(*WavReader::instance()),
+ mWavParser(*WaveformParser::instance()),
+ m_channelNumber(0),
+ m_isWaveformRepaired(false),
+ m_wavePos(0),
+ m_xScaleFactor(1),
+ m_yScaleFactor(80000),
+ m_clickState(WaitForFirstPress),
+ m_clickPosition(0),
+ m_selectionMode(false),
+ m_rangeSelected(false)
+{
+ setAcceptedMouseButtons(Qt::AllButtons);
+ setEnabled(true);
+}
+
+QWavVector* WaveformControl::getChannel(uint* chNum) const
+{
+ const auto c = chNum ? *chNum : m_channelNumber;
+ return c == 0 ? mWavReader.getChannel0() : mWavReader.getChannel1();
+}
+
+int WaveformControl::getWavPositionByMouseX(int x, int* point, double* dx) const
+{
+ const int xinc = getXScaleFactor() > 16.0 ? getXScaleFactor() / 16 : 1;
+ const double scale = boundingRect().width() * getXScaleFactor();
+ double tdx;
+ double& rdx = (dx ? *dx : tdx) = (boundingRect().width() / scale) * xinc;
+
+ int tpoint;
+ int& rpoint = (point ? *point : tpoint) = std::round(x / rdx);
+
+ return rpoint + getWavePos() * xinc;
+}
+
+void WaveformControl::paint(QPainter* painter)
+{
+ painter->setBackground(QBrush (QColor(m_selectionMode ? 37 : 7, m_selectionMode ? 37 : 7, 37)));
+ painter->setBackgroundMode(Qt::OpaqueMode);
+ painter->setPen(QColor(11, 60, 0));
+ const auto& bRect = boundingRect();
+ const double waveHeight = bRect.height() - 100;
+ const double halfHeight = waveHeight / 2;
+ painter->fillRect(bRect, painter->background());
+ painter->drawLine(0, halfHeight, bRect.width(), halfHeight);
+ int32_t scale = bRect.width() * getXScaleFactor();
+ int32_t pos = getWavePos();
+ const auto* channel = getChannel();
+ if (channel == nullptr) {
+ return;
+ }
+
+ const double maxy = getYScaleFactor();
+ double px = 0;
+ double py = bRect.height() / 2;
+ double x = 0;
+ double y = py;
+ const int xinc = getXScaleFactor() > 16.0 ? getXScaleFactor() / 16 : 1;
+ double dx = (bRect.width() / (double) scale) * xinc;
+ const auto parsedWaveform = mWavParser.getParsedWaveform(m_channelNumber);
+ bool printHint = false;
+ m_allowToGrabPoint = dx > 2;
+ const auto chsize = channel->size();
+ for (int32_t t = pos; t < pos + scale; t += xinc) {
+ if (t >= 0 && t < chsize) {
+ const int val = channel->operator[](t);
+ y = halfHeight - ((double) (val) / maxy) * waveHeight;
+ painter->setPen(val >= 0 ? QColor(50, 150, 0) : QColor(200, 0 , 0));
+ painter->drawLine(px, py, x, y);
+ if (m_allowToGrabPoint) {
+ painter->drawEllipse(QPoint(x, y), 2, 2);
+ }
+
+ const auto pwf = parsedWaveform[t];
+ if (pwf & mWavParser.sequenceMiddle) {
+ auto p = painter->pen();
+ p.setWidth(3);
+ painter->setPen(p);
+ painter->drawLine(px, bRect.height() - 15, x, bRect.height() - 15);
+ if (pwf & mWavParser.zeroBit || pwf & mWavParser.oneBit) {
+ p.setColor(QColor(0, 0, 250));
+ painter->setPen(p);
+ painter->drawLine(px, bRect.height() - 3, x, bRect.height() - 3);
+ }
+
+ if (printHint) {
+ QString text = pwf & mWavParser.pilotTone
+ ? "PILOT"
+ : pwf & mWavParser.synchroSignal
+ ? "SYNC"
+ : pwf & mWavParser.zeroBit
+ ? "\"0\""
+ : "\"1\"";
+ p.setWidth(1);
+ p.setColor(QColor(255, 255, 255));
+ painter->setPen(p);
+ painter->drawText(x + 3, bRect.height() - 15 - 10, text);
+ printHint = false;
+ }
+ }
+ else if (pwf & mWavParser.sequenceBegin || pwf & mWavParser.sequenceEnd) {
+ printHint = pwf & mWavParser.sequenceBegin;
+ auto p = painter->pen();
+ p.setWidth(3);
+ painter->setPen(p);
+ painter->drawLine(x, waveHeight + 2, x, bRect.height() - 15);
+
+ if (pwf & mWavParser.byteBound) {
+ p.setColor(pwf & mWavParser.sequenceBegin ? QColor(0, 0, 250) : QColor(255, 242, 0));
+ painter->setPen(p);
+ painter->drawLine(x, bRect.height() - 10, x, bRect.height() - 3);
+ }
+ }
+ }
+
+ px = x;
+ py = y;
+ x += dx;
+ }
+
+ if (m_selectionMode && m_rangeSelected) {
+ painter->setBackground(QBrush(QColor(7, 7, 137, 128)));
+ auto bRect = boundingRect();
+ bRect.setX(m_selectionRange.first);
+ bRect.setRight(m_selectionRange.second);
+ painter->fillRect(bRect, painter->background());
+ }
+}
+
+int32_t WaveformControl::getWavePos() const
+{
+ return m_wavePos;
+}
+
+int32_t WaveformControl::getWaveLength() const
+{
+ return getChannel()->size();
+}
+
+double WaveformControl::getXScaleFactor() const
+{
+ return m_xScaleFactor;
+}
+
+double WaveformControl::getYScaleFactor() const
+{
+ return m_yScaleFactor;
+}
+
+bool WaveformControl::getIsWaveformRepaired() const
+{
+ return m_isWaveformRepaired;
+}
+
+bool WaveformControl::getSelectionMode() const
+{
+ return m_selectionMode;
+}
+
+void WaveformControl::setWavePos(int32_t wavePos)
+{
+ if (m_wavePos != wavePos) {
+ m_wavePos = wavePos;
+ update();
+
+ emit wavePosChanged();
+ }
+}
+
+void WaveformControl::setXScaleFactor(double xScaleFactor)
+{
+ if (m_xScaleFactor != xScaleFactor) {
+ m_xScaleFactor = xScaleFactor;
+ update();
+
+ emit xScaleFactorChanged();
+ }
+}
+
+void WaveformControl::setYScaleFactor(double yScaleFactor)
+{
+ if (m_yScaleFactor != yScaleFactor) {
+ m_yScaleFactor = yScaleFactor;
+ update();
+
+ emit yScaleFactorChanged();
+ }
+}
+
+void WaveformControl::setSelectionMode(bool mode)
+{
+ if (m_selectionMode != mode) {
+ m_selectionMode = mode;
+ m_rangeSelected = false;
+ update();
+
+ emit selectionModeChanged();
+ }
+}
+
+void WaveformControl::mousePressEvent(QMouseEvent* event)
+{
+ if (!event) {
+ return;
+ }
+
+ //Checking for double-click
+ if (event->button() == Qt::LeftButton) {
+ auto now = QDateTime::currentDateTime();
+ qDebug() << "Click state: " << m_clickState << "; time: " << m_clickTime.msecsTo(now);
+ if (m_clickState == WaitForFirstPress) {
+ m_clickTime = QDateTime::currentDateTime();
+ m_clickState = WaitForFirstRelease;
+ }
+ else if (m_clickState == WaitForSecondPress && m_clickTime.msecsTo(now) <= 500) {
+ m_clickState = WaitForSecondRelease;
+ }
+ else {
+ m_clickState = WaitForFirstPress;
+ }
+ }
+
+ if (event->button() == Qt::LeftButton || event->button() == Qt::RightButton || event->button() == Qt::MiddleButton) {
+ double dx;
+ int point;
+ m_clickPosition = getWavPositionByMouseX(event->x(), &point, &dx);
+ event->accept();
+
+ if (m_selectionMode) {
+ m_rangeSelected = true;
+ m_selectionRange = { event->x(), event->x() };
+ update();
+ }
+
+ if (dx <= 2.0) {
+ return;
+ }
+ double dpoint = point * dx;
+ const double waveHeight = boundingRect().height() - 100;
+ const double halfHeight = waveHeight / 2;
+
+ if (!m_selectionMode) {
+ if (event->button() == Qt::MiddleButton ) {
+ //const auto p = point + getWavePos();
+ const auto d = getChannel()->operator[](m_clickPosition);
+ qDebug() << "Inserting point: " << m_clickPosition;
+ getChannel()->insert(m_clickPosition + (dpoint > event->x() ? 1 : -1), d);
+ update();
+ }
+ else {
+ if (dpoint >= event->x() - 2.0 && dpoint <= event->x() + 2) {
+ const double maxy = getYScaleFactor();
+ double y = halfHeight - ((double) (getChannel()->operator[](m_clickPosition)) / maxy) * waveHeight;
+ if (y >= event->y() - 2 && y <= event->y() + 2) {
+ if (event->button() == Qt::LeftButton) {
+ m_pointIndex = point;
+ m_pointGrabbed = true;
+ qDebug() << "Grabbed point: " << getChannel()->operator[](m_clickPosition);
+ }
+ else {
+ m_pointGrabbed = false;
+ getChannel()->remove(m_clickPosition);
+ update();
+ }
+ }
+ }
+ }
+ } // m_selectionMode
+ }
+}
+
+void WaveformControl::mouseReleaseEvent(QMouseEvent* event)
+{
+ if (!event) {
+ return;
+ }
+
+ switch (event->button()) {
+ case Qt::LeftButton:
+ {
+ //Double-click handling
+ auto now = QDateTime::currentDateTime();
+ qDebug() << "Click state: " << m_clickState << "; time: " << m_clickTime.msecsTo(now);
+
+ if (m_selectionMode && m_rangeSelected && m_selectionRange.first == m_selectionRange.second) {
+ m_rangeSelected = false;
+ }
+
+ if (m_clickState == WaitForFirstRelease && m_clickTime.msecsTo(now) <= 500) {
+ m_clickState = WaitForSecondPress;
+ }
+ else if (m_clickState == WaitForSecondRelease && m_clickTime.msecsTo(now) <= 500) {
+ m_clickState = WaitForFirstPress;
+ emit doubleClick(m_clickPosition);
+ }
+ else {
+ m_clickState = WaitForFirstPress;
+ }
+
+ m_pointGrabbed = false;
+ event->accept();
+ }
+ break;
+
+ default:
+ event->ignore();
+ break;
+ }
+}
+
+void WaveformControl::mouseMoveEvent(QMouseEvent* event)
+{
+ if (!event) {
+ return;
+ }
+
+ switch (event->buttons()) {
+ case Qt::LeftButton: {
+ if (m_selectionMode && m_rangeSelected) {
+ if (event->x() <= m_selectionRange.first) {
+ m_selectionRange.first = event->x();
+ }
+ else {
+ m_selectionRange.second = event->x();
+ }
+ }
+ else {
+ const auto* ch = getChannel();
+ if (!ch) {
+ return;
+ }
+
+ const double waveHeight = boundingRect().height() - 100;
+ const double halfHeight = waveHeight / 2;
+ const auto pointerPos = halfHeight - event->y();
+ double val = halfHeight + (m_yScaleFactor / waveHeight * pointerPos);
+ if (m_pointIndex + getWavePos() >= 0 && m_pointIndex + getWavePos() < ch->size()) {
+ getChannel()->operator[](m_pointIndex + getWavePos()) = val;
+ }
+ qDebug() << "Setting point: " << m_pointIndex + getWavePos();
+ }
+ event->accept();
+ update();
+ }
+ break;
+
+ default:
+ event->ignore();
+ break;
+ }
+}
+
+uint WaveformControl::getChannelNumber() const
+{
+ return m_channelNumber;
+}
+
+void WaveformControl::setChannelNumber(uint chNum)
+{
+ if (chNum != m_channelNumber) {
+ m_channelNumber = chNum;
+ emit channelNumberChanged();
+ }
+}
+
+void WaveformControl::reparse()
+{
+ mWavParser.parse(m_channelNumber);
+ update();
+}
+
+void WaveformControl::saveTap(const QString& fileUrl)
+{
+ QString fileName = fileUrl.isEmpty() ? fileUrl : QUrl(fileUrl).toLocalFile();
+ mWavParser.saveTap(m_channelNumber, fileName);
+}
+
+void WaveformControl::saveWaveform()
+{
+ mWavReader.saveWaveform();
+}
+
+void WaveformControl::repairWaveform()
+{
+ if (!m_isWaveformRepaired) {
+ //mWavReader.repairWaveform(m_channelNumber);
+ mWavReader.normalizeWaveform(0);
+ update();
+ m_isWaveformRepaired = true;
+ emit isWaveformRepairedChanged();
+ }
+}
+
+
+
+void WaveformControl::restoreWaveform()
+{
+ if (m_isWaveformRepaired) {
+ mWavReader.restoreWaveform(m_channelNumber);
+ update();
+ m_isWaveformRepaired = false;
+ emit isWaveformRepairedChanged();
+ }
+}
+
+void WaveformControl::shiftWaveform()
+{
+ mWavReader.shiftWaveform(m_channelNumber);
+ update();
+}
+
+void WaveformControl::copySelectedToAnotherChannel()
+{
+ if (m_selectionMode && m_rangeSelected) {
+ uint destChNum = getChannelNumber() == 0 ? 1 : 0;
+ const auto sourceChannel = getChannel();
+ const auto destChannel = getChannel(&destChNum);
+ const auto endIdx = getWavPositionByMouseX(m_selectionRange.second);
+ for (auto i = getWavPositionByMouseX(m_selectionRange.first); i <= endIdx; ++i) {
+ destChannel->operator[](i) = sourceChannel->operator[](i);
+ }
+ }
+}
diff --git a/sources/controls/waveformcontrol.h b/sources/controls/waveformcontrol.h
new file mode 100644
index 0000000..f02fb2e
--- /dev/null
+++ b/sources/controls/waveformcontrol.h
@@ -0,0 +1,100 @@
+//*******************************************************************************
+// ZX Tape Reviver
+//-----------------
+//
+// Author: Leonid Golouz
+// E-mail: lgolouz@list.ru
+//*******************************************************************************
+
+#ifndef WAVEFORMCONTROL_H
+#define WAVEFORMCONTROL_H
+
+#include
+#include
+#include "sources/core/wavreader.h"
+#include "sources/core/waveformparser.h"
+
+class WaveformControl : public QQuickPaintedItem
+{
+ Q_OBJECT
+
+ Q_PROPERTY(uint channelNumber READ getChannelNumber WRITE setChannelNumber NOTIFY channelNumberChanged)
+ Q_PROPERTY(int wavePos READ getWavePos WRITE setWavePos NOTIFY wavePosChanged)
+ Q_PROPERTY(int waveLength READ getWaveLength NOTIFY waveLengthChanged)
+ Q_PROPERTY(double xScaleFactor READ getXScaleFactor WRITE setXScaleFactor NOTIFY xScaleFactorChanged)
+ Q_PROPERTY(double yScaleFactor READ getYScaleFactor WRITE setYScaleFactor NOTIFY yScaleFactorChanged)
+ Q_PROPERTY(bool isWaveformRepaired READ getIsWaveformRepaired NOTIFY isWaveformRepairedChanged)
+ Q_PROPERTY(bool selectionMode READ getSelectionMode WRITE setSelectionMode NOTIFY selectionModeChanged)
+
+ WavReader& mWavReader;
+ WaveformParser& mWavParser;
+
+public:
+ explicit WaveformControl(QQuickItem* parent = nullptr);
+
+ uint getChannelNumber() const;
+ int32_t getWavePos() const;
+ int32_t getWaveLength() const;
+ double getXScaleFactor() const;
+ double getYScaleFactor() const;
+ bool getIsWaveformRepaired() const;
+ bool getSelectionMode() const;
+
+ void setChannelNumber(uint chNum);
+ void setWavePos(int wavPos);
+ void setXScaleFactor(double xScaleFactor);
+ void setYScaleFactor(double yScaleFactor);
+ void setSelectionMode(bool mode);
+
+ virtual void paint(QPainter* painter) override;
+ virtual void mousePressEvent(QMouseEvent* event) override;
+ virtual void mouseReleaseEvent(QMouseEvent* event) override;
+ virtual void mouseMoveEvent(QMouseEvent *event) override;
+
+ Q_INVOKABLE void reparse();
+ Q_INVOKABLE void saveTap(const QString& fileUrl = QString());
+ Q_INVOKABLE void saveWaveform();
+ Q_INVOKABLE void repairWaveform();
+ Q_INVOKABLE void restoreWaveform();
+ Q_INVOKABLE void shiftWaveform();
+ Q_INVOKABLE void copySelectedToAnotherChannel();
+
+signals:
+ void channelNumberChanged();
+ void wavePosChanged();
+ void waveLengthChanged();
+ void xScaleFactorChanged();
+ void yScaleFactorChanged();
+ void isWaveformRepairedChanged();
+ void selectionModeChanged();
+
+ void doubleClick(int idx);
+
+private:
+ enum ClickStates {
+ WaitForFirstPress,
+ WaitForSecondPress,
+ WaitForFirstRelease,
+ WaitForSecondRelease
+ };
+
+ uint m_channelNumber;
+ bool m_isWaveformRepaired;
+ bool m_allowToGrabPoint;
+ bool m_pointGrabbed;
+ int m_pointIndex;
+ int m_wavePos;
+ double m_xScaleFactor;
+ double m_yScaleFactor;
+ ClickStates m_clickState;
+ QDateTime m_clickTime;
+ int m_clickPosition;
+ bool m_selectionMode;
+ bool m_rangeSelected;
+ QPair m_selectionRange;
+
+ QWavVector* getChannel(uint* chNum = nullptr) const;
+ int getWavPositionByMouseX(int x, int* point = nullptr, double* dx = nullptr) const;
+};
+
+#endif // WAVEFORMCONTROL_H
diff --git a/sources/core/waveformparser.cpp b/sources/core/waveformparser.cpp
new file mode 100644
index 0000000..6a7db2b
--- /dev/null
+++ b/sources/core/waveformparser.cpp
@@ -0,0 +1,348 @@
+//*******************************************************************************
+// ZX Tape Reviver
+//-----------------
+//
+// Author: Leonid Golouz
+// E-mail: lgolouz@list.ru
+//*******************************************************************************
+
+#include "waveformparser.h"
+#include
+#include
+#include
+
+WaveformParser::WaveformParser(QObject* parent) :
+ QObject(parent),
+ mWavReader(*WavReader::instance())
+{
+
+}
+
+void WaveformParser::parse(uint chNum)
+{
+ if (chNum >= mWavReader.getNumberOfChannels()) {
+ qDebug() << "Trying to parse channel that exceeds overall number of channels";
+ return;
+ }
+
+ QWavVector& channel = *(chNum == 0 ? mWavReader.getChannel0() : mWavReader.getChannel1());
+ QVector parsed = parseChannel(channel);
+
+ const double sampleRate = mWavReader.getSampleRate();
+ auto isFreqFitsInDelta = [&sampleRate](uint32_t length, uint32_t signalFreq, double signalDelta, double deltaDivider = 1.0) -> bool {
+ const double freq = (sampleRate / length);
+ const double delta = signalFreq * (signalDelta / deltaDivider);
+ return freq >= (signalFreq - delta) && freq <= (signalFreq + delta);
+ };
+
+ auto& parsedData = mParsedData[chNum];
+ parsedData.clear();
+
+ auto& parsedWaveform = mParsedWaveform[chNum];
+ parsedWaveform.resize(channel.size());
+
+ auto currentState = SEARCH_OF_PILOT_TONE;
+ auto it = parsed.begin();
+ QVector data;
+ //WaveformSign signalDirection = POSITIVE;
+ uint32_t dataStart = 0;
+ int8_t bitIndex = 7;
+ uint8_t bit = 0;
+ uint8_t parity = 0;
+
+ while (currentState != NO_MORE_DATA) {
+ auto prevIt = it;
+ switch (currentState) {
+ case SEARCH_OF_PILOT_TONE:
+ it = std::find_if(it, parsed.end(), [&isFreqFitsInDelta, chNum, this](const WaveformPart& p) {
+ fillParsedWaveform(chNum, p, 0);
+ return isFreqFitsInDelta(p.length, SignalFrequencies::PILOT_HALF_FREQ, pilotDelta, 2.0);
+ });
+ if (it != parsed.end()) {
+ currentState = PILOT_TONE;
+ }
+ break;
+
+ case PILOT_TONE:
+ it = std::find_if(it, parsed.end(), [&isFreqFitsInDelta, chNum, this](const WaveformPart& p) {
+ fillParsedWaveform(chNum, p, pilotTone ^ sequenceMiddle);
+ return isFreqFitsInDelta(p.length, SignalFrequencies::SYNCHRO_FIRST_HALF_FREQ, synchroDelta);
+ });
+ if (it != parsed.end()) {
+ auto eIt = std::prev(it);
+ fillParsedWaveform(chNum, *eIt, pilotTone ^ sequenceMiddle);
+ parsedWaveform[prevIt->begin] = pilotTone ^ sequenceBegin;
+ parsedWaveform[eIt->end] = pilotTone ^ sequenceEnd;
+
+ currentState = SYNCHRO_SIGNAL;
+// WaveformData wd;
+// wd.begin = std::distance(parsed.begin(), prevIt);
+// wd.end = std::distance(parsed.begin(), std::prev(it));
+// wd.waveBegin = prevIt->begin;
+// wd.waveEnd = it->end;
+// wd.value = SYNCHRO;
+
+// mParsedWaveform.insert(wd.waveBegin, wd);
+ }
+ break;
+
+ case SYNCHRO_SIGNAL:
+ it = std::next(it);
+ if (it != parsed.end()) {
+ if ((isFreqFitsInDelta(it->length, SignalFrequencies::SYNCHRO_SECOND_HALF, synchroDelta)) && (isFreqFitsInDelta(it->length + prevIt->length, SignalFrequencies::SYNCHRO_FREQ, synchroDelta, 2.0))) {
+ fillParsedWaveform(chNum, *prevIt, synchroSignal ^ sequenceMiddle);
+ fillParsedWaveform(chNum, *it, synchroSignal ^ sequenceMiddle);
+ parsedWaveform[prevIt->begin] = synchroSignal ^ sequenceBegin;
+ parsedWaveform[it->end] = synchroSignal ^ sequenceEnd;
+
+ currentState = DATA_SIGNAL;
+ //signalDirection = prevIt->sign;
+
+// WaveformData wd;
+// wd.begin = std::distance(parsed.begin(), prevIt);
+// wd.end = std::distance(parsed.begin(), it);
+// wd.waveBegin = prevIt->begin;
+// wd.waveEnd = it->end;
+// wd.value = SYNCHRO;
+
+// mParsedWaveform.insert(wd.waveBegin, wd);
+ it = std::next(it);
+ dataStart = std::distance(parsed.begin(), it) + 1;
+ data.clear();
+ bitIndex = 7;
+ bit ^= bit;
+ }
+ else {
+ currentState = SEARCH_OF_PILOT_TONE;
+ }
+ }
+ break;
+
+ case DATA_SIGNAL:
+ it = std::next(it);
+ if (it != parsed.end()) {
+ const auto len = it->length + prevIt->length;
+ if (isFreqFitsInDelta(len, SignalFrequencies::ZERO_FREQ, zeroDelta)) { //ZERO
+ fillParsedWaveform(chNum, *prevIt, zeroBit ^ sequenceMiddle);
+ fillParsedWaveform(chNum, *it, zeroBit ^ sequenceMiddle);
+ parsedWaveform[prevIt->begin] = zeroBit ^ sequenceBegin;
+ parsedWaveform[it->end] = zeroBit ^ sequenceEnd;
+
+ if (bitIndex == 7) {
+ parsedWaveform[prevIt->begin] ^= byteBound;
+ }
+
+// WaveformData wd;
+// wd.begin = std::distance(parsed.begin(), prevIt);
+// wd.end = std::distance(parsed.begin(), it);
+// wd.waveBegin = prevIt->begin;
+// wd.waveEnd = it->end;
+// wd.value = SignalValue::ZERO;
+
+// mParsedWaveform.insert(wd.waveBegin, wd);
+ --bitIndex;
+ if (bitIndex < 0) {
+ parsedWaveform[it->end] ^= byteBound;
+ bitIndex = 7;
+ data.append(bit);
+ parity ^= bit;
+ bit ^= bit;
+ }
+ }
+ else if (isFreqFitsInDelta(len, SignalFrequencies::ONE_FREQ, oneDelta)) { //ONE
+ fillParsedWaveform(chNum, *prevIt, oneBit ^ sequenceMiddle);
+ fillParsedWaveform(chNum, *it, oneBit ^ sequenceMiddle);
+ parsedWaveform[prevIt->begin] = oneBit ^ sequenceBegin;
+ parsedWaveform[it->end] = oneBit ^ sequenceEnd;
+
+// WaveformData wd;
+// wd.begin = std::distance(parsed.begin(), prevIt);
+// wd.end = std::distance(parsed.begin(), it);
+// wd.waveBegin = prevIt->begin;
+// wd.waveEnd = it->end;
+// wd.value = SignalValue::ONE;
+
+// mParsedWaveform.insert(wd.waveBegin, wd);
+ bit |= 1 << bitIndex--;
+ if (bitIndex < 0) {
+ parsedWaveform[it->end] ^= byteBound;
+ bitIndex = 7;
+ data.append(bit);
+ parity ^= bit;
+ bit ^= bit;
+ }
+ }
+ else {
+ currentState = END_OF_DATA;
+ if (!data.empty()) {
+ DataBlock db;
+ db.dataStart = parsed.at(dataStart).begin;
+ db.dataEnd = parsed.at(std::distance(parsed.begin(), it)).end;
+ db.data = data;
+ 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
+ parity ^= parity; //Zeroing parity byte
+
+ parsedData.append(db);
+ }
+ }
+ it = std::next(it);
+ }
+ else if (!data.empty()) {
+ DataBlock db;
+ db.dataStart = parsed.at(dataStart).begin;
+ db.dataEnd = parsed.at(parsed.size() - 1).end;
+ db.data = data;
+ 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
+ parity ^= parity; //Zeroing parity byte
+
+ parsedData.append(db);
+ }
+ break;
+
+ case END_OF_DATA:
+ currentState = SEARCH_OF_PILOT_TONE;
+ break;
+
+ default:
+ break;
+ }
+
+ if (it == parsed.end()) {
+ currentState = NO_MORE_DATA;
+ }
+
+ }
+
+ if (chNum == 0) {
+ emit parsedChannel0Changed();
+ }
+ else {
+ emit parsedChannel1Changed();
+ }
+}
+
+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);
+
+ auto& parsedData = mParsedData[chNum];
+ for (auto i = 0; i < parsedData.size(); ++i) {
+ QByteArray b;
+ const uint16_t size = parsedData.at(i).data.size();
+ b.append(reinterpret_cast(&size), sizeof(size));
+ for (uint8_t c: parsedData.at(i).data) {
+ b.append(c);
+ }
+ f.write(b);
+ }
+
+ f.close();
+}
+
+void WaveformParser::fillParsedWaveform(uint chNum, const WaveformPart& p, uint8_t val)
+{
+ auto& parsedWaveform = mParsedWaveform[chNum];
+ for (auto i = p.begin; i < p.end; ++i) {
+ parsedWaveform[i] = val;
+ }
+}
+
+QVector WaveformParser::getParsedWaveform(uint chNum) const
+{
+ return mParsedWaveform[chNum];
+}
+
+int WaveformParser::getBlockDataStart(uint chNum, uint blockNum) const
+{
+ if (chNum < mParsedData.size() && blockNum < mParsedData[chNum].size()) {
+ return mParsedData[chNum][blockNum].dataStart;
+ }
+ return 0;
+}
+
+int WaveformParser::getBlockDataEnd(uint chNum, uint blockNum) const
+{
+ if (chNum < mParsedData.size() && blockNum < mParsedData[chNum].size()) {
+ return mParsedData[chNum][blockNum].dataEnd;
+ }
+ return 0;
+}
+
+QVariantList WaveformParser::getParsedChannelData(uint chNum) const
+{
+ static QMap blockTypes {
+ {0x00, "Program"},
+ {0x01, "Number Array"},
+ {0x02, "Character Array"},
+ {0x03, "Bytes"}
+ };
+
+ QVariantList r;
+ const auto& ch = mParsedData[chNum];
+ uint blockNumber = 1;
+
+ for (const auto& i: ch) {
+ QVariantMap m;
+
+ m.insert("blockNumber", blockNumber++);
+ if (i.data.size() > 0) {
+ auto d = i.data.at(0);
+ int blockType = -1;
+ auto btIt = blockTypes.end();
+ QString blockTypeName;
+ if (d == 0x00 && i.data.size() > 1) {
+ d = i.data.at(1);
+ btIt = blockTypes.find(d);
+ blockType = btIt == blockTypes.end() ? -1 : d;
+ blockTypeName = blockType == -1 ? QString::number(d, 16) : *btIt;
+ }
+ else {
+ blockType = -2;
+ blockTypeName = d == 0x00 ? "Header" : "Code";
+ }
+ m.insert("blockType", blockTypeName);
+ QString sizeText = QString::number(i.data.size());
+ if (i.data.size() > 13 && btIt != blockTypes.end()) {
+ sizeText += QString(" (%1)").arg(i.data.at(13) * 256 + i.data.at(12));
+ }
+ m.insert("blockSize", sizeText);
+ QString nameText;
+ if (blockType >= 0) {
+ for (auto idx = 2; idx < std::min(12, i.data.size()); ++idx) {
+ nameText += QChar(i.data.at(idx));
+ }
+ }
+ m.insert("blockName", nameText);
+ m.insert("blockStatus", i.state == OK ? "Ok" : "Error");
+ m.insert("state", i.state);
+ }
+ else {
+ m.insert("blockType", "Unknown");
+ m.insert("blockName", QString());
+ m.insert("blockSize", 0);
+ m.insert("blockStatus", "Unknown");
+ }
+ r.append(m);
+ }
+
+ return r;
+}
+
+QVariantList WaveformParser::getParsedChannel0() const
+{
+ return getParsedChannelData(0);
+}
+
+QVariantList WaveformParser::getParsedChannel1() const
+{
+ return getParsedChannelData(1);
+}
+
+WaveformParser* WaveformParser::instance()
+{
+ static WaveformParser p;
+ return &p;
+}
diff --git a/sources/core/waveformparser.h b/sources/core/waveformparser.h
new file mode 100644
index 0000000..6af8810
--- /dev/null
+++ b/sources/core/waveformparser.h
@@ -0,0 +1,154 @@
+//*******************************************************************************
+// ZX Tape Reviver
+//-----------------
+//
+// Author: Leonid Golouz
+// E-mail: lgolouz@list.ru
+//*******************************************************************************
+
+#ifndef WAVEFORMPARSER_H
+#define WAVEFORMPARSER_H
+
+#include
+#include
+#include
+#include
+#include
+#include "sources/core/wavreader.h"
+#include "sources/defines.h"
+
+class WaveformParser : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QVariantList parsedChannel0 READ getParsedChannel0 NOTIFY parsedChannel0Changed)
+ Q_PROPERTY(QVariantList parsedChannel1 READ getParsedChannel1 NOTIFY parsedChannel1Changed)
+
+public:
+// enum SignalValue { ZERO, ONE, PILOT, SYNCHRO };
+// struct WaveformData
+// {
+// uint32_t begin;
+// uint32_t end;
+// uint32_t waveBegin;
+// uint32_t waveEnd;
+// SignalValue value;
+// };
+
+// |------------------------------- 1 - zero '0' data bit
+// | |----------------------------- 1 - one '1' data bit
+// | | |--------------------------- 1 - pilot tone
+// | | | |------------------------- 1 - synchro signal
+// | | | | |----------------------- 1 - byte bound
+// | | | | | |--------------------- 1 - begin of signal sequence
+// | | | | | | |------------------- 1 - middle of signal sequence
+// | | | | | | | |----------------- 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
+
+private:
+ enum WaveformSign { POSITIVE, NEGATIVE };
+ enum StateType { SEARCH_OF_PILOT_TONE, PILOT_TONE, SYNCHRO_SIGNAL, DATA_SIGNAL, END_OF_DATA, NO_MORE_DATA };
+ enum SignalFrequencies {
+ PILOT_HALF_FREQ = 1620,
+ PILOT_FREQ = 810,
+ SYNCHRO_FIRST_HALF_FREQ = 4900,
+ SYNCHRO_SECOND_HALF = 5500,
+ SYNCHRO_FREQ = 2600,
+ ZERO_HALF_FREQ = 4090,
+ ZERO_FREQ = 2050,
+ ONE_HALF_FREQ = 2045,
+ ONE_FREQ = 1023
+ };
+ enum DataState { OK, R_TAPE_LOADING_ERROR };
+
+ 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;
+
+ struct WaveformPart
+ {
+ uint32_t begin;
+ uint32_t end;
+ uint32_t length;
+ WaveformSign sign;
+ };
+
+ struct DataBlock
+ {
+ uint32_t dataStart;
+ uint32_t dataEnd;
+ QVector data;
+ DataState state;
+ };
+
+ template
+ QVector parseChannel(const QVector& ch) {
+ QVector result;
+ if (ch.size() < 1) {
+ return result;
+ }
+
+ auto it = ch.begin();
+ auto val = *it;
+ 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;
+ part.begin = std::distance(ch.begin(), prevIt);
+ part.end = std::distance(ch.begin(), std::prev(it));
+ part.length = std::distance(prevIt, it);
+ part.sign = lessThanZero(val) ? NEGATIVE : POSITIVE;
+
+ result.append(part);
+
+ if (it != ch.end()) {
+ val = *it;
+ }
+ }
+
+ return result;
+ }
+
+ void fillParsedWaveform(uint chNum, const WaveformPart& p, uint8_t val);
+
+ WavReader& mWavReader;
+ QMap> mParsedWaveform;
+ QMap> mParsedData;
+
+protected:
+ explicit WaveformParser(QObject* parent = nullptr);
+ QVariantList getParsedChannelData(uint chNum) const;
+
+public:
+ virtual ~WaveformParser() override = default;
+
+ static WaveformParser* instance();
+
+ void parse(uint chNum);
+ void saveTap(uint chNum, const QString& fileName = QString());
+ void saveWaveform(uint chNum);
+ QVector getParsedWaveform(uint chNum) const;
+
+ Q_INVOKABLE int getBlockDataStart(uint chNum, uint blockNum) const;
+ Q_INVOKABLE int getBlockDataEnd(uint chNum, uint blockNum) const;
+
+ //getters
+ QVariantList getParsedChannel0() const;
+ QVariantList getParsedChannel1() const;
+
+signals:
+ void parsedChannel0Changed();
+ void parsedChannel1Changed();
+};
+
+#endif // WAVEFORMPARSER_H
diff --git a/sources/core/wavreader.cpp b/sources/core/wavreader.cpp
new file mode 100644
index 0000000..3c873d6
--- /dev/null
+++ b/sources/core/wavreader.cpp
@@ -0,0 +1,342 @@
+//*******************************************************************************
+// ZX Tape Reviver
+//-----------------
+//
+// Author: Leonid Golouz
+// E-mail: lgolouz@list.ru
+//*******************************************************************************
+
+#include "wavreader.h"
+#include
+#include
+#include
+#include
+
+WavReader::WavReader(QObject* parent) :
+ QObject(parent),
+ mWavOpened(false),
+ mChannel0(nullptr),
+ mChannel1(nullptr)
+{
+
+}
+
+WavReader::ErrorCodesEnum WavReader::setFileName(const QString& fileName)
+{
+ if (!mWavOpened) {
+ mWavFile.setFileName(fileName);
+ return Ok;
+ }
+ return AlreadyOpened;
+}
+
+WavReader::ErrorCodesEnum WavReader::open()
+{
+ if (!mWavOpened) {
+ mWavOpened = mWavFile.open(QIODevice::ReadOnly);
+ if (mWavOpened) {
+ mWavOpened = false;
+ QByteArray buf;
+
+ {
+ const WavHeader* riffHeader = readData(buf);
+ if (!riffHeader || riffHeader->chunk.chunkId != riffId || riffHeader->riffType != waveId) {
+ mWavFile.close();
+ return InvalidWavFormat;
+ }
+ }
+
+ {
+ const WavFmt* fmtHeader = readData(buf);
+ if (!fmtHeader || fmtHeader->chunk.chunkId != fmt_Id) {
+ mWavFile.close();
+ return InvalidWavFormat;
+ }
+
+ if ((fmtHeader->compressionCode != 1 && fmtHeader->compressionCode != 3) || (fmtHeader->numberOfChannels != 1 && fmtHeader->numberOfChannels != 2) ||
+ (fmtHeader->significantBitsPerSample != 8 && fmtHeader->significantBitsPerSample != 16 && fmtHeader->significantBitsPerSample != 24 && fmtHeader->significantBitsPerSample != 32)
+ ) {
+ mWavFile.close();
+ return UnsupportedWavFormat;
+ }
+
+ mWavFormatHeader = *fmtHeader;
+ }
+
+ {
+ const WavChunk* dataHeader = readData(buf);
+ if (!dataHeader || dataHeader->chunkId != dataId) {
+ mWavFile.close();
+ return InvalidWavFormat;
+ }
+
+ mCurrentChunk = *dataHeader;
+ }
+
+ mWavOpened = true;
+ return Ok;
+ }
+
+ return CantOpen;
+ }
+
+ return AlreadyOpened;
+}
+
+QWavVectorType WavReader::getSample(QByteArray& buf, size_t& bufIndex, uint dataSize, uint compressionCode)
+{
+ QWavVectorType r { };
+ void* v;
+ v = reinterpret_cast(getData(buf, bufIndex, dataSize));
+
+ switch (dataSize) {
+ case 1:
+ r = (*reinterpret_cast(v) - 128) * 258.;
+ break;
+
+ case 2:
+ r = *reinterpret_cast(v);
+ break;
+
+ case 3:
+ {
+ Int24* t;
+ t = reinterpret_cast(v);
+ r = ((t->b2 << 16) | (t->b1 << 8) | t->b0);
+ }
+ break;
+
+ case 4:
+ r = compressionCode == 3 ? *reinterpret_cast(v) : *reinterpret_cast(v);
+ break;
+
+ default:
+ qDebug() << "Unsupported data size";
+ break;
+ }
+
+ return r;
+}
+
+QWavVector* WavReader::createVector(size_t bytesPerSample, size_t size)
+{
+ Q_UNUSED(bytesPerSample)
+ return new QWavVector(static_cast(size));
+}
+
+WavReader::ErrorCodesEnum WavReader::read()
+{
+ if (!mWavOpened) {
+ return NotOpened;
+ }
+
+ QByteArray buf { mWavFile.read(mCurrentChunk.chunkDataSize) };
+ if (buf.size() < static_cast(mCurrentChunk.chunkDataSize)) {
+ return InsufficientData;
+ }
+
+ mChannel0.reset(nullptr);
+ mChannel1.reset(nullptr);
+
+ size_t bytesPerSample = mWavFormatHeader.significantBitsPerSample / 8;
+ size_t numSamples = mCurrentChunk.chunkDataSize / (bytesPerSample * mWavFormatHeader.numberOfChannels);
+
+ mChannel0.reset(createVector(bytesPerSample, numSamples));
+ if (mWavFormatHeader.numberOfChannels == 2) {
+ mChannel1.reset(createVector(bytesPerSample, numSamples));
+ }
+
+ size_t bufIndex = 0;
+ QVector channelBufIndex(mWavFormatHeader.numberOfChannels, 0);
+ for (size_t i = 0; i < numSamples; ++i) {
+ for (int channelNum = 0; channelNum < mWavFormatHeader.numberOfChannels; ++channelNum) {
+ auto& channel = channelNum == 0 ? mChannel0 : mChannel1;
+ auto& cbi = channelBufIndex[channelNum];
+
+ channel->operator[](cbi) = getSample(buf, bufIndex, bytesPerSample, mWavFormatHeader.compressionCode);
+ ++cbi;
+ }
+ }
+
+ emit numberOfChannelsChanged();
+ return Ok;
+}
+
+uint WavReader::getNumberOfChannels() const
+{
+ return mWavOpened ? mWavFormatHeader.numberOfChannels : 0;
+}
+
+uint32_t WavReader::getSampleRate() const
+{
+ return mWavOpened ? mWavFormatHeader.sampleRate : 0;
+}
+
+uint WavReader::getBytesPerSample() const
+{
+ return mWavOpened ? mWavFormatHeader.significantBitsPerSample / 8 : 0;
+}
+
+QWavVector* WavReader::getChannel0() const
+{
+ return mChannel0.get();
+}
+
+QWavVector* WavReader::getChannel1() const
+{
+ return mChannel1.get();
+}
+
+WavReader::ErrorCodesEnum WavReader::close()
+{
+ if (!mWavOpened) {
+ return NotOpened;
+ }
+ mWavOpened = false;
+
+ mWavFile.close();
+ return Ok;
+}
+
+void WavReader::saveWaveform() const
+{
+ QFile f(QString("waveform_%1.wfm").arg(QDateTime::currentDateTime().toString("dd.MM.yyyy hh-mm-ss.zzz")));
+ f.open(QIODevice::ReadWrite);
+
+ const auto& ch = *getChannel0();
+ QByteArray b;
+ for (auto i = 0; i < ch.size(); ++i) {
+ const QWavVectorType val = ch[i];
+ b.append(reinterpret_cast(&val), sizeof(val));
+ }
+ f.write(b);
+ f.close();
+}
+
+void WavReader::shiftWaveform(uint chNum)
+{
+ if (chNum >= mWavFormatHeader.numberOfChannels) {
+ qDebug() << "Channel number exceeds number of channels";
+ return;
+ }
+
+ auto& storedCh = mStoredChannels[chNum];
+ auto& ch = chNum == 0 ? mChannel0 : mChannel1;
+ storedCh.reset(new QWavVector(*ch.get()));
+ for (auto i = 0; i < mChannel0->size(); ++i) {
+ ch->operator[](i) -= 1000;
+ }
+}
+
+void WavReader::storeWaveform(uint chNum)
+{
+ if (chNum >= mWavFormatHeader.numberOfChannels) {
+ qDebug() << "Channel number exceeds number of channels";
+ return;
+ }
+
+ auto& ch = chNum == 0 ? mChannel0 : mChannel1;
+ mStoredChannels[chNum].reset(new QWavVector(*ch));
+}
+
+void WavReader::restoreWaveform(uint chNum)
+{
+ if (chNum >= mWavFormatHeader.numberOfChannels) {
+ qDebug() << "Channel number exceeds number of channels";
+ return;
+ }
+
+ auto& ch = chNum == 0 ? mChannel0 : mChannel1;
+ auto& storedCh = mStoredChannels[chNum];
+ if (storedCh.isNull()) {
+ storedCh.reset(new QWavVector());
+ }
+
+ ch.reset(new QWavVector(*mStoredChannels[chNum]));
+}
+
+void WavReader::normalizeWaveform(uint chNum)
+{
+ if (chNum >= mWavFormatHeader.numberOfChannels) {
+ qDebug() << "Channel number exceeds number of channels";
+ return;
+ }
+
+ auto& ch = *(chNum == 0 ? mChannel0 : mChannel1).get();
+ auto haveSameSign = [](QWavVectorType o1, QWavVectorType o2) {
+ return lessThanZero(o1) == lessThanZero(o2);
+ };
+
+ //Trying to find a sine
+ auto bIt = ch.begin();
+
+ while (bIt != ch.end()) {
+ //qDebug() << "bIt: " << std::distance(ch.begin(), bIt) << "; end: " << std::distance(ch.begin(), ch.end());
+// auto itPos = std::distance(ch.begin(), bIt);
+// auto prc = ((float) itPos / ch.size()) * 100;
+// qDebug() << "Total: " << (int) prc;
+
+ auto prevIt = bIt;
+ auto it = std::next(prevIt);
+ QMap::iterator> peaks {{0, bIt}};
+
+ for (int i = 1; i < 4; ++i) {
+ //down-to-up part when i == 1, 3
+ //up-to-down part when i == 2
+ bool finished = true;
+ for (; it != ch.end();) {
+ if (haveSameSign(*prevIt, *it)) {
+ if ((i == 2 ? std::abs(*prevIt) >= std::abs(*it) : std::abs(*prevIt) <= std::abs(*it))) {
+ prevIt = it;
+ it = std::next(it);
+ }
+ else {
+ peaks[i] = it;
+ break;
+ }
+ }
+ else {
+ bIt = it;
+ finished = false;
+ it = ch.end();
+ }
+ }
+
+ //Signal crosses zero level - not ours case
+ if (it == ch.end()) {
+ if (finished) {
+ bIt = it;
+ }
+ break;
+ }
+ }
+
+ //Looks like we've found a sine, normalizing it
+ if (it != ch.end()) {
+ bIt = it;
+ for (auto i = 0; i < 3; ++i) {
+ auto middlePoint = std::distance(peaks[i], peaks[i + 1]) / 2;
+ auto middleIt = std::next(peaks[i], middlePoint);
+ auto middleVal = *middleIt;
+ auto incVal = QWavVectorType(-1) * middleVal;
+ std::for_each(i == 1 ? middleIt : peaks[i], i == 1 ? peaks[i + 1] : middleIt, [incVal](QWavVectorType& i) {
+// qDebug() << "Old: " << i << "; new: " << (i+incVal);
+ i += incVal;
+ });
+ }
+ }
+ }
+}
+
+WavReader::~WavReader()
+{
+ if (mWavOpened) {
+ mWavFile.close();
+ }
+}
+
+WavReader* WavReader::instance()
+{
+ static WavReader w;
+ return &w;
+}
diff --git a/sources/core/wavreader.h b/sources/core/wavreader.h
new file mode 100644
index 0000000..3444001
--- /dev/null
+++ b/sources/core/wavreader.h
@@ -0,0 +1,152 @@
+//*******************************************************************************
+// ZX Tape Reviver
+//-----------------
+//
+// Author: Leonid Golouz
+// E-mail: lgolouz@list.ru
+//*******************************************************************************
+
+#ifndef WAVREADER_H
+#define WAVREADER_H
+
+#include
+#include
+#include
+#include "sources/defines.h"
+
+class WavReader : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(uint numberOfChannels READ getNumberOfChannels NOTIFY numberOfChannelsChanged)
+
+private:
+//Disable struct alignment
+#pragma pack(push, 1)
+ struct Int24 {
+ uint8_t b0;
+ uint8_t b1;
+ uint8_t b2;
+ };
+
+ struct WavChunk
+ {
+ uint32_t chunkId;
+ uint32_t chunkDataSize;
+ };
+
+ struct WavHeader
+ {
+ WavChunk chunk;
+ uint32_t riffType;
+ };
+
+ struct WavFmt
+ {
+ WavChunk chunk;
+ uint16_t compressionCode;
+ uint16_t numberOfChannels;
+ uint32_t sampleRate;
+ uint32_t avgBytesPerSecond;
+ uint16_t blockAlign;
+ uint16_t significantBitsPerSample;
+ };
+
+ template
+ struct WavMonoSample
+ {
+ T channel;
+ };
+
+ template
+ struct WavStereoSample
+ {
+ T leftChannel;
+ T rightChannel;
+ };
+#pragma pack(pop)
+
+ const uint32_t riffId = 0x46464952; //"RIFF"
+ const uint32_t waveId = 0x45564157; //"WAVE"
+ const uint32_t fmt_Id = 0x20746D66; //"fmt "
+ const uint32_t dataId = 0x61746164; //"data"
+
+ template
+ const T* readData(QByteArray& buf) {
+ buf = mWavFile.read(sizeof(T));
+ if (buf.size() < sizeof(T)) {
+ return nullptr;
+ }
+ return reinterpret_cast(buf.data());
+ }
+
+ template
+ T* getData(QByteArray& buf, size_t& bufIndex) {
+ T* res = reinterpret_cast(buf.data() + bufIndex);
+ bufIndex += sizeof(T);
+ return res;
+ }
+
+ uint8_t* getData(QByteArray& buf, size_t& bufIndex, uint dataSize) {
+ if (dataSize == 0) {
+ return nullptr;
+ }
+
+ auto t = getData(buf, bufIndex);
+ bufIndex += dataSize - 1;
+ return t;
+ }
+
+ QWavVectorType getSample(QByteArray& buf, size_t& bufIndex, uint dataSize, uint compressionCode);
+ QWavVector* createVector(size_t bytesPerSample, size_t size);
+
+ WavFmt mWavFormatHeader;
+ WavChunk mCurrentChunk;
+ bool mWavOpened;
+ QFile mWavFile;
+ QScopedPointer mChannel0;
+ QScopedPointer mChannel1;
+ QMap> mStoredChannels;
+
+protected:
+ explicit WavReader(QObject* parent = nullptr);
+
+public:
+ enum ErrorCodesEnum {
+ Ok,
+ AlreadyOpened,
+ CantOpen,
+ NotOpened,
+ InvalidWavFormat,
+ UnsupportedWavFormat,
+ InsufficientData,
+ EndOfBuffer
+ };
+ Q_ENUM(ErrorCodesEnum)
+
+ virtual ~WavReader() override;
+
+ uint getNumberOfChannels() const;
+ uint32_t getSampleRate() const;
+ uint getBytesPerSample() const;
+ QWavVector* /*const*/ getChannel0() const;
+ QWavVector* /*const*/ getChannel1() const;
+
+ ErrorCodesEnum setFileName(const QString& fileName);
+ ErrorCodesEnum open();
+ ErrorCodesEnum read();
+ ErrorCodesEnum close();
+
+ void saveWaveform() const;
+ void shiftWaveform(uint chNum);
+ void storeWaveform(uint chNum);
+ void restoreWaveform(uint chNum);
+ void normalizeWaveform(uint chNum);
+
+ static WavReader* instance();
+
+signals:
+ void numberOfChannelsChanged();
+};
+
+#endif // WAVREADER_H
diff --git a/sources/main.cpp b/sources/main.cpp
new file mode 100644
index 0000000..d1f2995
--- /dev/null
+++ b/sources/main.cpp
@@ -0,0 +1,51 @@
+//*******************************************************************************
+// ZX Tape Reviver
+//-----------------
+//
+// Author: Leonid Golouz
+// E-mail: lgolouz@list.ru
+//*******************************************************************************
+
+#include
+#include
+#include "sources/controls/waveformcontrol.h"
+#include "sources/core/waveformparser.h"
+#include "sources/models/fileworkermodel.h"
+
+void registerTypes()
+{
+ qmlRegisterUncreatableType("com.enums.zxtapereviver", 1, 0, "FileWorkerResults", QString());
+ qmlRegisterUncreatableType("com.enums.zxtapereviver", 1, 0, "ErrorCodesEnum", QString());
+
+ qmlRegisterType("WaveformControl", 1, 0, "WaveformControl");
+ qmlRegisterSingletonType("com.models.zxtapereviver", 1, 0, "FileWorkerModel", [](QQmlEngine* engine, QJSEngine* scriptEngine) -> QObject* {
+ Q_UNUSED(engine)
+ Q_UNUSED(scriptEngine)
+ return new FileWorkerModel();
+ });
+ qmlRegisterSingletonInstance("com.core.zxtapereviver", 1, 0, "WavReader", WavReader::instance());
+ qmlRegisterSingletonInstance("com.core.zxtapereviver", 1, 0, "WaveformParser", WaveformParser::instance());
+}
+
+int main(int argc, char *argv[])
+{
+ Q_UNUSED(argc)
+ Q_UNUSED(argv)
+
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+
+ QGuiApplication app(argc, argv);
+
+ registerTypes();
+
+ QQmlApplicationEngine engine;
+ const QUrl url(QStringLiteral("qrc:/main.qml"));
+ QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app, [url](QObject *obj, const QUrl &objUrl) {
+ if (!obj && url == objUrl) {
+ QCoreApplication::exit(-1);
+ }
+ }, Qt::QueuedConnection);
+ engine.load(url);
+
+ return app.exec();
+}
diff --git a/sources/models/fileworkermodel.cpp b/sources/models/fileworkermodel.cpp
new file mode 100644
index 0000000..f2847a6
--- /dev/null
+++ b/sources/models/fileworkermodel.cpp
@@ -0,0 +1,53 @@
+//*******************************************************************************
+// ZX Tape Reviver
+//-----------------
+//
+// Author: Leonid Golouz
+// E-mail: lgolouz@list.ru
+//*******************************************************************************
+
+#include "fileworkermodel.h"
+#include "sources/core/waveformparser.h"
+#include
+#include
+
+FileWorkerModel::FileWorkerModel(QObject* parent) :
+ QObject(parent),
+ m_wavFileName(QString())
+{
+
+}
+
+/*WavReader::ErrorCodesEnum*/ int FileWorkerModel::openWavFileByUrl(const QString& fileNameUrl)
+{
+ QUrl u(fileNameUrl);
+ return openWavFile(u.toLocalFile());
+}
+
+/*WavReader::ErrorCodesEnum*/ int FileWorkerModel::openWavFile(const QString& fileName)
+{
+ auto& r = *WavReader::instance();
+ r.close();
+
+ auto result = r.setFileName(fileName);
+ result = r.open();
+ if (result == WavReader::Ok) {
+ result = r.read();
+ if (result == WavReader::Ok) {
+ m_wavFileName = fileName;
+ emit wavFileNameChanged();
+ }
+ }
+
+ return result;
+}
+
+QString FileWorkerModel::getWavFileName() const
+{
+ return m_wavFileName;
+}
+
+FileWorkerModel::~FileWorkerModel()
+{
+ qDebug() << "~FileWorkerModel";
+}
diff --git a/sources/models/fileworkermodel.h b/sources/models/fileworkermodel.h
new file mode 100644
index 0000000..2e631b3
--- /dev/null
+++ b/sources/models/fileworkermodel.h
@@ -0,0 +1,46 @@
+//*******************************************************************************
+// ZX Tape Reviver
+//-----------------
+//
+// Author: Leonid Golouz
+// E-mail: lgolouz@list.ru
+//*******************************************************************************
+
+#ifndef FILEWORKERMODEL_H
+#define FILEWORKERMODEL_H
+
+#include
+#include "sources/core/wavreader.h"
+
+class FileWorkerModel : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString wavFileName READ getWavFileName NOTIFY wavFileNameChanged)
+
+public:
+ enum FileWorkerResults {
+ FW_OK,
+ FW_ERR
+ };
+ Q_ENUM(FileWorkerResults)
+
+ explicit FileWorkerModel(QObject* parent = nullptr);
+ ~FileWorkerModel();
+ //getters
+ QString getWavFileName() const;
+
+ //setters
+
+ //QML invocable members
+ Q_INVOKABLE /*WavReader::ErrorCodesEnum*/ int openWavFileByUrl(const QString& fileNameUrl);
+ Q_INVOKABLE /*WavReader::ErrorCodesEnum*/ int openWavFile(const QString& fileName);
+
+signals:
+ void wavFileNameChanged();
+
+private:
+ QString m_wavFileName;
+};
+
+#endif // FILEWORKERMODEL_H