# HG changeset patch # User Chris Cannam # Date 1592325846 -3600 # Node ID cb9209ef373a70b8f5c8ca3bc722fad504c590f9 # Parent 7b6e18380e8fd6a83a7f17aa2aa099a5bc5444a7# Parent 44dba7cd9ec335b05fd905adecc353e7955611fc Merge from default branch diff -r 7b6e18380e8f -r cb9209ef373a base/Event.h --- a/base/Event.h Wed Jun 03 13:57:50 2020 +0100 +++ b/base/Event.h Tue Jun 16 17:44:06 2020 +0100 @@ -84,7 +84,10 @@ m_haveDuration(true), m_haveReferenceFrame(false), m_value(value), m_level(0.f), m_frame(frame), m_duration(duration), m_referenceFrame(0), m_label(label) { - if (m_duration < 0) throw std::logic_error("duration must be >= 0"); + if (m_duration < 0) { + m_frame += m_duration; + m_duration = -m_duration; + } } Event(sv_frame_t frame, float value, sv_frame_t duration, @@ -93,7 +96,10 @@ m_haveDuration(true), m_haveReferenceFrame(false), m_value(value), m_level(level), m_frame(frame), m_duration(duration), m_referenceFrame(0), m_label(label) { - if (m_duration < 0) throw std::logic_error("duration must be >= 0"); + if (m_duration < 0) { + m_frame += m_duration; + m_duration = -m_duration; + } } Event(const Event &event) =default; @@ -135,7 +141,10 @@ Event p(*this); p.m_duration = duration; p.m_haveDuration = true; - if (duration < 0) throw std::logic_error("duration must be >= 0"); + if (p.m_duration < 0) { + p.m_frame += p.m_duration; + p.m_duration = -p.m_duration; + } return p; } Event withoutDuration() const { diff -r 7b6e18380e8f -r cb9209ef373a base/Preferences.cpp --- a/base/Preferences.cpp Wed Jun 03 13:57:50 2020 +0100 +++ b/base/Preferences.cpp Tue Jun 16 17:44:06 2020 +0100 @@ -44,6 +44,7 @@ m_omitRecentTemps(true), m_tempDirRoot(""), m_fixedSampleRate(0), + m_recordMono(false), m_resampleOnLoad(false), m_gapless(true), m_normaliseAudio(false), @@ -67,6 +68,7 @@ (settings.value("window-type", int(HanningWindow)).toInt()); m_runPluginsInProcess = settings.value("run-vamp-plugins-in-process", true).toBool(); m_fixedSampleRate = settings.value("fixed-sample-rate", 0).toDouble(); + m_recordMono = settings.value("record-mono", false).toBool(); m_resampleOnLoad = settings.value("resample-on-load", false).toBool(); m_gapless = settings.value("gapless", true).toBool(); m_normaliseAudio = settings.value("normalise-audio", false).toBool(); @@ -100,6 +102,7 @@ props.push_back("Window Type"); props.push_back("Resample Quality"); props.push_back("Omit Temporaries from Recent Files"); + props.push_back("Record Mono"); props.push_back("Resample On Load"); props.push_back("Use Gapless Mode"); props.push_back("Normalise Audio"); @@ -141,6 +144,9 @@ if (name == "Omit Temporaries from Recent Files") { return tr("Omit temporaries from Recent Files menu"); } + if (name == "Record Mono") { + return tr("Mix recorded channels to mono"); + } if (name == "Resample On Load") { return tr("Resample mismatching files on import"); } @@ -201,6 +207,9 @@ if (name == "Omit Temporaries from Recent Files") { return ToggleProperty; } + if (name == "Record Mono") { + return ToggleProperty; + } if (name == "Resample On Load") { return ToggleProperty; } @@ -552,6 +561,19 @@ } void +Preferences::setRecordMono(bool mono) +{ + if (m_recordMono != mono) { + m_recordMono = mono; + QSettings settings; + settings.beginGroup("Preferences"); + settings.setValue("record-mono", mono); + settings.endGroup(); + emit propertyChanged("Record Mono"); + } +} + +void Preferences::setResampleOnLoad(bool resample) { if (m_resampleOnLoad != resample) { diff -r 7b6e18380e8f -r cb9209ef373a base/Preferences.h --- a/base/Preferences.h Wed Jun 03 13:57:50 2020 +0100 +++ b/base/Preferences.h Tue Jun 16 17:44:06 2020 +0100 @@ -65,6 +65,10 @@ QString getTemporaryDirectoryRoot() const { return m_tempDirRoot; } + /// True if we should always mix down recorded audio to a single + /// channel regardless of how many channels the device opens + bool getRecordMono() const { return m_recordMono; } + /// If we should always resample audio to the same rate, return it; otherwise (the normal case) return 0 sv_samplerate_t getFixedSampleRate() const { return m_fixedSampleRate; } @@ -119,6 +123,7 @@ void setOmitTempsFromRecentFiles(bool omit); void setTemporaryDirectoryRoot(QString tempDirRoot); void setFixedSampleRate(sv_samplerate_t); + void setRecordMono(bool); void setResampleOnLoad(bool); void setUseGaplessMode(bool); void setNormaliseAudio(bool); @@ -157,6 +162,7 @@ bool m_omitRecentTemps; QString m_tempDirRoot; sv_samplerate_t m_fixedSampleRate; + bool m_recordMono; bool m_resampleOnLoad; bool m_gapless; bool m_normaliseAudio; diff -r 7b6e18380e8f -r cb9209ef373a data/fileio/CSVFileReader.cpp --- a/data/fileio/CSVFileReader.cpp Wed Jun 03 13:57:50 2020 +0100 +++ b/data/fileio/CSVFileReader.cpp Tue Jun 16 17:44:06 2020 +0100 @@ -117,17 +117,18 @@ return m_error; } -sv_frame_t +bool CSVFileReader::convertTimeValue(QString s, int lineno, sv_samplerate_t sampleRate, - int windowSize) const + int windowSize, + sv_frame_t &calculatedFrame) const { QRegExp nonNumericRx("[^0-9eE.,+-]"); int warnLimit = 10; CSVFormat::TimeUnits timeUnits = m_format.getTimeUnits(); - sv_frame_t calculatedFrame = 0; + calculatedFrame = 0; bool ok = false; QString numeric = s; @@ -316,31 +317,37 @@ switch (modelType) { case CSVFormat::OneDimensionalModel: + SVDEBUG << "CSVFileReader: Creating sparse one-dimensional model" << endl; model1 = new SparseOneDimensionalModel(sampleRate, windowSize); model = model1; break; case CSVFormat::TwoDimensionalModel: + SVDEBUG << "CSVFileReader: Creating sparse time-value model" << endl; model2 = new SparseTimeValueModel(sampleRate, windowSize, false); model = model2; break; case CSVFormat::TwoDimensionalModelWithDuration: + SVDEBUG << "CSVFileReader: Creating region model" << endl; model2a = new RegionModel(sampleRate, windowSize, false); model = model2a; break; case CSVFormat::TwoDimensionalModelWithDurationAndPitch: + SVDEBUG << "CSVFileReader: Creating note model" << endl; model2b = new NoteModel(sampleRate, windowSize, false); model = model2b; break; case CSVFormat::TwoDimensionalModelWithDurationAndExtent: + SVDEBUG << "CSVFileReader: Creating box model" << endl; model2c = new BoxModel(sampleRate, windowSize, false); model = model2c; break; case CSVFormat::ThreeDimensionalModel: + SVDEBUG << "CSVFileReader: Creating editable dense three-dimensional model" << endl; model3 = new EditableDenseThreeDimensionalModel (sampleRate, windowSize, valueColumns); model = model3; @@ -348,6 +355,7 @@ case CSVFormat::WaveFileModel: { + SVDEBUG << "CSVFileReader: Creating writable wave-file model" << endl; bool normalise = (m_format.getAudioSampleRange() == CSVFormat::SampleRangeOther); QString path = getConvertedAudioFilePath(); @@ -387,6 +395,7 @@ float otherValue = 0.f; float pitch = 0.f; QString label = ""; + bool ok = true; duration = 0.f; haveEndTime = false; @@ -403,16 +412,21 @@ break; case CSVFormat::ColumnStartTime: - frameNo = convertTimeValue(s, lineno, sampleRate, windowSize); + if (!convertTimeValue(s, lineno, sampleRate, windowSize, frameNo)) { + ok = false; + } break; case CSVFormat::ColumnEndTime: - endFrame = convertTimeValue(s, lineno, sampleRate, windowSize); - haveEndTime = true; + if (convertTimeValue(s, lineno, sampleRate, windowSize, endFrame)) { + haveEndTime = true; + } break; case CSVFormat::ColumnDuration: - duration = convertTimeValue(s, lineno, sampleRate, windowSize); + if (!convertTimeValue(s, lineno, sampleRate, windowSize, duration)) { + ok = false; + } break; case CSVFormat::ColumnValue: @@ -436,6 +450,10 @@ } } + if (!ok) { + continue; + } + ++labelCountMap[label]; if (haveEndTime) { // ... calculate duration now all cols read diff -r 7b6e18380e8f -r cb9209ef373a data/fileio/CSVFileReader.h --- a/data/fileio/CSVFileReader.h Wed Jun 03 13:57:50 2020 +0100 +++ b/data/fileio/CSVFileReader.h Tue Jun 16 17:44:06 2020 +0100 @@ -70,8 +70,8 @@ mutable int m_progress; ProgressReporter *m_reporter; - sv_frame_t convertTimeValue(QString, int lineno, sv_samplerate_t sampleRate, - int windowSize) const; + bool convertTimeValue(QString, int lineno, sv_samplerate_t sampleRate, + int windowSize, sv_frame_t &calculatedFrame) const; QString getConvertedAudioFilePath() const; }; diff -r 7b6e18380e8f -r cb9209ef373a data/fileio/test/CSVReaderTest.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/fileio/test/CSVReaderTest.h Tue Jun 16 17:44:06 2020 +0100 @@ -0,0 +1,234 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef TEST_CSV_READER_H +#define TEST_CSV_READER_H + +#include "../CSVFileReader.h" + +#include "data/model/SparseOneDimensionalModel.h" +#include "data/model/SparseTimeValueModel.h" +#include "data/model/RegionModel.h" +#include "data/model/EditableDenseThreeDimensionalModel.h" + +#include "base/Debug.h" + +#include + +#include +#include +#include + +#include + +using namespace std; + +class CSVReaderTest : public QObject +{ + Q_OBJECT + +private: + QDir csvDir; + sv_samplerate_t mainRate; + +public: + CSVReaderTest(QString base) { + if (base == "") { + base = "svcore/data/fileio/test"; + } + csvDir = QDir(base + "/csv"); + mainRate = 44100; + } + +private: + void loadFrom(QString filename, Model *&model) { + QString path(csvDir.filePath(filename)); + CSVFormat f; + f.guessFormatFor(path); + CSVFileReader reader(path, f, mainRate); + model = reader.load(); + QVERIFY(model); + QVERIFY(reader.isOK()); + QCOMPARE(reader.getError(), QString()); + } + +private slots: + void init() { + if (!csvDir.exists()) { + SVCERR << "ERROR: CSV test file directory \"" << csvDir.absolutePath() << "\" does not exist" << endl; + QVERIFY2(csvDir.exists(), "CSV test file directory not found"); + } + } + + void modelType1DSamples() { + Model *model = nullptr; + loadFrom("model-type-1d-samples.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getAllEvents().size(), 5); + //!!! + the actual contents + delete model; + } + + void modelType1DSeconds() { + Model *model = nullptr; + loadFrom("model-type-1d-seconds.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getAllEvents().size(), 5); + delete model; + } + + void modelType2DDurationSamples() { + Model *model = nullptr; + loadFrom("model-type-2d-duration-samples.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getAllEvents().size(), 5); + delete model; + } + + void modelType2DDurationSeconds() { + Model *model = nullptr; + loadFrom("model-type-2d-duration-seconds.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getAllEvents().size(), 5); + delete model; + } + + void badNegativeDuration() { + Model *model = nullptr; + loadFrom("bad-negative-duration.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + //!!! + check duration has been corrected + QCOMPARE(actual->getAllEvents().size(), 5); + delete model; + } + + void modelType2DEndTimeSamples() { + Model *model = nullptr; + loadFrom("model-type-2d-endtime-samples.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getAllEvents().size(), 5); + delete model; + } + + void modelType2DEndTimeSeconds() { + Model *model = nullptr; + loadFrom("model-type-2d-endtime-seconds.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getAllEvents().size(), 5); + delete model; + } + + void modelType2DImplicit() { + Model *model = nullptr; + loadFrom("model-type-2d-implicit.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getAllEvents().size(), 5); + delete model; + } + + void modelType2DSamples() { + Model *model = nullptr; + loadFrom("model-type-2d-samples.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getAllEvents().size(), 5); + delete model; + } + + void modelType2DSeconds() { + Model *model = nullptr; + loadFrom("model-type-2d-seconds.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getAllEvents().size(), 5); + delete model; + } + + void modelType3DImplicit() { + Model *model = nullptr; + loadFrom("model-type-3d-implicit.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getWidth(), 6); + QCOMPARE(actual->getHeight(), 6); + delete model; + } + + void modelType3DSamples() { + Model *model = nullptr; + loadFrom("model-type-3d-samples.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getWidth(), 6); + QCOMPARE(actual->getHeight(), 6); + delete model; + } + + void modelType3DSeconds() { + Model *model = nullptr; + loadFrom("model-type-3d-seconds.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getWidth(), 6); + QCOMPARE(actual->getHeight(), 6); + delete model; + } + + void withBlankLines1D() { + Model *model = nullptr; + loadFrom("with-blank-lines-1d.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getAllEvents().size(), 5); + delete model; + } + + void withBlankLines2D() { + Model *model = nullptr; + loadFrom("with-blank-lines-2d.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getAllEvents().size(), 5); + delete model; + } + + void withBlankLines3D() { + Model *model = nullptr; + loadFrom("with-blank-lines-3d.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getWidth(), 6); + QCOMPARE(actual->getHeight(), 6); + delete model; + } + + void quoting() { + Model *model = nullptr; + loadFrom("quoting.csv", model); + auto actual = qobject_cast(model); + QVERIFY(actual); + QCOMPARE(actual->getAllEvents().size(), 5); + delete model; + } +}; + +#endif diff -r 7b6e18380e8f -r cb9209ef373a data/fileio/test/csv/bad-negative-duration.csv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/fileio/test/csv/bad-negative-duration.csv Tue Jun 16 17:44:06 2020 +0100 @@ -0,0 +1,6 @@ +# As model-type-2d-duration-seconds.csv but with a negative value for a duration +1.1,4,620 +2.2,4.2,880 +3.3,0.4,440 +4.4,3.8,213 +5.5,-2.3,123 diff -r 7b6e18380e8f -r cb9209ef373a data/fileio/test/csv/model-type-2d-duration-seconds.csv --- a/data/fileio/test/csv/model-type-2d-duration-seconds.csv Wed Jun 03 13:57:50 2020 +0100 +++ b/data/fileio/test/csv/model-type-2d-duration-seconds.csv Tue Jun 16 17:44:06 2020 +0100 @@ -5,4 +5,4 @@ 2.2,4.2,880 3.3,0.4,440 4.4,3.8,213 -5.5,-2.3,123 +5.5,2.3,123 diff -r 7b6e18380e8f -r cb9209ef373a data/fileio/test/csv/quoting.csv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/fileio/test/csv/quoting.csv Tue Jun 16 17:44:06 2020 +0100 @@ -0,0 +1,5 @@ +1,2,Label 1 +"2",4 +3,"6","Labels 3a, 3b" +4,8.0,"Label \"4\"" +5, 1,"Label ""5""" \ No newline at end of file diff -r 7b6e18380e8f -r cb9209ef373a data/fileio/test/csv/with-blank-lines-1d.csv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/fileio/test/csv/with-blank-lines-1d.csv Tue Jun 16 17:44:06 2020 +0100 @@ -0,0 +1,9 @@ + +3.2 First thing + +4.4 Second thing +5.5 Third thing + +6.3 Fourth thing +7.8 Fifth thing + diff -r 7b6e18380e8f -r cb9209ef373a data/fileio/test/csv/with-blank-lines-2d.csv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/fileio/test/csv/with-blank-lines-2d.csv Tue Jun 16 17:44:06 2020 +0100 @@ -0,0 +1,12 @@ + +45678,4 + +123239,4.2 + 320130,0.4 + +# Also include some CR/LF variations: +452103,3.8 + + +# And let's not have a newline after this last line: +620301,-2.3 \ No newline at end of file diff -r 7b6e18380e8f -r cb9209ef373a data/fileio/test/csv/with-blank-lines-3d.csv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/fileio/test/csv/with-blank-lines-3d.csv Tue Jun 16 17:44:06 2020 +0100 @@ -0,0 +1,10 @@ + +22050,143.0,2.0,-1.3,0.0,0.0,1.0 +44100,0.2,0.1,-3.0,0.0,0.1,0.143 + +66150,0.143,0.2,-3.1,0.0,0.0,0.1 +88200,2.0,1.0,-0.3,0.0,1.0,143.0 + +110250,0.0,0.0,0.1,0.143,0.2,-3.1 +132300,0.0,1.0,143.0,2.0,1.0,-0.3 + diff -r 7b6e18380e8f -r cb9209ef373a data/fileio/test/files.pri --- a/data/fileio/test/files.pri Wed Jun 03 13:57:50 2020 +0100 +++ b/data/fileio/test/files.pri Tue Jun 16 17:44:06 2020 +0100 @@ -9,6 +9,7 @@ EncodingTest.h \ MIDIFileReaderTest.h \ CSVFormatTest.h \ + CSVReaderTest.h \ CSVStreamWriterTest.h TEST_SOURCES += \ diff -r 7b6e18380e8f -r cb9209ef373a data/fileio/test/svcore-data-fileio-test.cpp --- a/data/fileio/test/svcore-data-fileio-test.cpp Wed Jun 03 13:57:50 2020 +0100 +++ b/data/fileio/test/svcore-data-fileio-test.cpp Tue Jun 16 17:44:06 2020 +0100 @@ -18,6 +18,7 @@ #include "EncodingTest.h" #include "MIDIFileReaderTest.h" #include "CSVFormatTest.h" +#include "CSVReaderTest.h" #include "CSVStreamWriterTest.h" #include "system/Init.h" @@ -90,6 +91,12 @@ } { + CSVReaderTest t(testDir); + if (QTest::qExec(&t, argc, argv) == 0) ++good; + else ++bad; + } + + { CSVStreamWriterTest t; if (QTest::qExec(&t, argc, argv) == 0) ++good; else ++bad; diff -r 7b6e18380e8f -r cb9209ef373a data/model/AlignmentModel.cpp --- a/data/model/AlignmentModel.cpp Wed Jun 03 13:57:50 2020 +0100 +++ b/data/model/AlignmentModel.cpp Tue Jun 16 17:44:06 2020 +0100 @@ -29,7 +29,8 @@ m_reversePath(nullptr), m_pathBegun(false), m_pathComplete(false), - m_relativePitch(0) + m_relativePitch(0), + m_explicitlySetCompletion(-1) { setPathFrom(pathSource); @@ -105,6 +106,10 @@ bool AlignmentModel::isReady(int *completion) const { + if (m_explicitlySetCompletion != -1) { + if (completion) *completion = m_explicitlySetCompletion; + return (m_explicitlySetCompletion == 100); + } if (!m_pathBegun && !m_pathSource.isNone()) { if (completion) *completion = 0; #ifdef DEBUG_ALIGNMENT_MODEL @@ -156,6 +161,13 @@ return m_aligned; } +void +AlignmentModel::setCompletion(int completion) +{ + m_explicitlySetCompletion = completion; + emit completionChanged(getId()); +} + sv_frame_t AlignmentModel::toReference(sv_frame_t frame) const { @@ -310,9 +322,9 @@ AlignmentModel::performAlignment(const Path &path, sv_frame_t frame) const { // The path consists of a series of points, each with frame equal - // to the frame on the source model and mapframe equal to the - // frame on the target model. Both should be monotonically - // increasing. + // to the frame on the source model (aligned model) and mapframe + // equal to the frame on the target model (reference model). Both + // should be monotonically increasing. const Path::Points &points = path.getPoints(); diff -r 7b6e18380e8f -r cb9209ef373a data/model/AlignmentModel.h --- a/data/model/AlignmentModel.h Wed Jun 03 13:57:50 2020 +0100 +++ b/data/model/AlignmentModel.h Tue Jun 16 17:44:06 2020 +0100 @@ -56,6 +56,8 @@ ModelId getReferenceModel() const; ModelId getAlignedModel() const; + void setCompletion(int completion); + sv_frame_t toReference(sv_frame_t frame) const; sv_frame_t fromReference(sv_frame_t frame) const; @@ -107,6 +109,7 @@ bool m_pathComplete; QString m_error; int m_relativePitch; + int m_explicitlySetCompletion; void constructPath() const; void constructReversePath() const; diff -r 7b6e18380e8f -r cb9209ef373a data/model/Path.h --- a/data/model/Path.h Wed Jun 03 13:57:50 2020 +0100 +++ b/data/model/Path.h Tue Jun 16 17:44:06 2020 +0100 @@ -29,6 +29,11 @@ PathPoint(sv_frame_t _frame, sv_frame_t _mapframe) : frame(_frame), mapframe(_mapframe) { } + // "The path consists of a series of points, each with frame equal + // to the frame on the source model (aligned model) and mapframe + // equal to the frame on the target model (reference model). Both + // should be monotonically increasing." + sv_frame_t frame; sv_frame_t mapframe;