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