Skip to content

Commit 0baa392

Browse files
committed
Merge branch 'feature-midiformatconverter'
2 parents f031ccd + fa11f04 commit 0baa392

16 files changed

+730
-226
lines changed

src/libs/application/uishell/src/MIDITrackSelectorDialog.h

Lines changed: 0 additions & 57 deletions
This file was deleted.

src/libs/application/uishell/src/MIDITrackSelectorStringConverter.h

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/plugins/CMakeLists.txt

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ set(CURRENT_PLUGIN_DESC "${APPLICATION_NAME} Application Plugin")
77
find_package(ExtensionSystem REQUIRED)
88

99
function(diffscope_add_builtin_plugin _target)
10-
set(options NO_TRANSLATION NO_QML_MODULE NO_ACTION)
10+
set(options NO_TRANSLATION NO_QML_MODULE NO_ACTION MACOS_INCLUDE_OBJC_SRC)
1111
set(oneValueArgs NAME DISPLAY_NAME MACRO_PREFIX QML_MODULE_NAME)
12-
set(multiValueArgs QAK_DEFINES)
12+
set(multiValueArgs QAK_DEFINES WIN_EXCLUDE_SRC MACOS_EXCLUDE_SRC LINUX_EXCLUDE_SRC)
1313
cmake_parse_arguments(FUNC "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
1414

1515
set(_plugin_name ${FUNC_NAME})
@@ -26,6 +26,32 @@ function(diffscope_add_builtin_plugin _target)
2626

2727
file(GLOB_RECURSE _src *.h *.cpp)
2828

29+
if(APPLE AND FUNC_MACOS_INCLUDE_OBJC_SRC)
30+
file(GLOB_RECURSE _objc_src *.m *.mm)
31+
list(APPEND _src ${_objc_src})
32+
endif()
33+
34+
if(WIN32 AND FUNC_WIN_EXCLUDE_SRC)
35+
foreach(_pattern IN LISTS FUNC_WIN_EXCLUDE_SRC)
36+
file(GLOB_RECURSE _win_excluded ${_pattern})
37+
list(REMOVE_ITEM _src ${_win_excluded})
38+
endforeach()
39+
endif()
40+
41+
if(APPLE AND FUNC_MACOS_EXCLUDE_SRC)
42+
foreach(_pattern IN LISTS FUNC_MACOS_EXCLUDE_SRC)
43+
file(GLOB_RECURSE _mac_excluded ${_pattern})
44+
list(REMOVE_ITEM _src ${_mac_excluded})
45+
endforeach()
46+
endif()
47+
48+
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND FUNC_LINUX_EXCLUDE_SRC)
49+
foreach(_pattern IN LISTS FUNC_LINUX_EXCLUDE_SRC)
50+
file(GLOB_RECURSE _linux_excluded ${_pattern})
51+
list(REMOVE_ITEM _src ${_linux_excluded})
52+
endforeach()
53+
endif()
54+
2955
if(NOT FUNC_NO_ACTION)
3056
qak_add_action_extension(
3157
_actions_src

src/plugins/midiformatconverter/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,21 @@ project(midiformatconverter
33
DESCRIPTION ${CURRENT_PLUGIN_DESC}
44
)
55

6+
set(_links_icu)
7+
if(WIN32)
8+
list(APPEND _links_icu icu)
9+
elseif(NOT APPLE)
10+
find_package(ICU COMPONENTS uc REQUIRED)
11+
list(APPEND _links_icu ICU::uc)
12+
endif()
13+
614
diffscope_add_builtin_plugin(${PROJECT_NAME}
715
NAME org.diffscope.midiformatconverter
816
DISPLAY_NAME "MIDI Format Converter"
917
MACRO_PREFIX MIDI_FORMAT_CONVERTER
1018
QML_MODULE_NAME MIDIFormatConverter
1119
NO_ACTION
20+
MACOS_INCLUDE_OBJC_SRC
1221
QT_LINKS
1322
Core
1423
Gui
@@ -22,6 +31,9 @@ diffscope_add_builtin_plugin(${PROJECT_NAME}
2231
coreplugin
2332
importexportmanager
2433
opendspx::model
34+
opendspx::converter
35+
${_links_icu}
2536
INCLUDE_PRIVATE
2637
internal/**
38+
MACOS_EXCLUDE_SRC *_icu.cpp
2739
)

src/plugins/midiformatconverter/internal/MIDIFileExporter.cpp

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
11
#include "MIDIFileExporter.h"
22

3-
#include <QtCore/QtGlobal>
3+
#include <QCheckBox>
4+
#include <QComboBox>
5+
#include <QDialog>
6+
#include <QDialogButtonBox>
7+
#include <QGroupBox>
8+
#include <QHBoxLayout>
9+
#include <QLabel>
10+
#include <QSet>
11+
#include <QVBoxLayout>
12+
#include <QSaveFile>
13+
#include <QLoggingCategory>
14+
#include <QDir>
15+
16+
#include <CoreApi/runtimeinterface.h>
17+
18+
#include <SVSCraftQuick/MessageBox.h>
19+
20+
#include <opendspx/model.h>
21+
#include <opendspxconverter/midi/midiconverter.h>
22+
#include <opendspxconverter/midi/midiintermediatedata.h>
23+
24+
#include <midiformatconverter/internal/MIDITextCodecConverter.h>
425

526
namespace MIDIFormatConverter::Internal {
627

28+
Q_STATIC_LOGGING_CATEGORY(lcMIDIFileExporter, "diffscope.midiformatconverter.midifileexporter")
29+
730
MIDIFileExporter::MIDIFileExporter(QObject *parent) : FileConverter(parent) {
831
setName(tr("MIDI"));
932
setDescription(tr("Export as Standard MIDI file"));
@@ -15,7 +38,69 @@ namespace MIDIFormatConverter::Internal {
1538
MIDIFileExporter::~MIDIFileExporter() = default;
1639

1740
bool MIDIFileExporter::execExport(const QString &path, const QDspx::Model &model, QWindow *window) {
18-
// TODO
41+
QDialog dialog;
42+
dialog.setWindowTitle(tr("MIDI Export"));
43+
44+
auto *mainLayout = new QVBoxLayout(&dialog);
45+
46+
auto *optionsGroup = new QGroupBox(tr("Options"), &dialog);
47+
auto *optionsLayout = new QVBoxLayout(optionsGroup);
48+
49+
auto *encodingLayout = new QHBoxLayout;
50+
auto *encodingLabel = new QLabel(tr("Encoding"), optionsGroup);
51+
auto *encodingComboBox = new QComboBox(optionsGroup);
52+
const QByteArray utf8Name = QStringConverter::nameForEncoding(QStringConverter::Utf8);
53+
int defaultEncodingIndex = -1;
54+
55+
const auto codecs = MIDITextCodecConverter::availableCodecs();
56+
for (const auto &codec : codecs) {
57+
const int row = encodingComboBox->count();
58+
encodingComboBox->addItem(codec.displayName, codec.identifier);
59+
if (defaultEncodingIndex < 0 && codec.identifier == MIDITextCodecConverter::defaultCodec())
60+
defaultEncodingIndex = row;
61+
}
62+
63+
if (defaultEncodingIndex >= 0)
64+
encodingComboBox->setCurrentIndex(defaultEncodingIndex);
65+
else if (encodingComboBox->count() > 0)
66+
encodingComboBox->setCurrentIndex(0);
67+
encodingLayout->addWidget(encodingLabel);
68+
encodingLayout->addWidget(encodingComboBox, 1);
69+
optionsLayout->addLayout(encodingLayout);
70+
71+
auto *separateCheckBox = new QCheckBox(tr("Separate singing clips"), optionsGroup);
72+
optionsLayout->addWidget(separateCheckBox);
73+
74+
optionsGroup->setLayout(optionsLayout);
75+
mainLayout->addWidget(optionsGroup);
76+
77+
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
78+
connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
79+
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
80+
mainLayout->addWidget(buttonBox);
81+
82+
if (dialog.exec() != QDialog::Accepted) {
83+
return false;
84+
}
85+
const QByteArray selectedEncoding = encodingComboBox->currentData().toByteArray();
86+
const bool separateClips = separateCheckBox->isChecked();
87+
88+
auto intermediateData = QDspx::MidiConverter::convertDspxToIntermediate(model, [selectedEncoding](const QString &text) {
89+
return MIDITextCodecConverter::encode(text, selectedEncoding);
90+
}, {480, separateClips});
91+
auto data = QDspx::MidiConverter::convertIntermediateToMidi(intermediateData);
92+
QSaveFile file(path);
93+
if (!file.open(QIODevice::WriteOnly)) {
94+
qCCritical(lcMIDIFileExporter) << "Failed to write file:" << path << file.errorString();
95+
SVS::MessageBox::critical(Core::RuntimeInterface::qmlEngine(), window, tr("Failed to save file"), QStringLiteral("%1\n\n%2").arg(QDir::toNativeSeparators(path), file.errorString()));
96+
return false;
97+
}
98+
file.write(data);
99+
if (!file.commit()) {
100+
qCCritical(lcMIDIFileExporter) << "Failed to commit file:" << path << file.errorString();
101+
SVS::MessageBox::critical(Core::RuntimeInterface::qmlEngine(), window, tr("Failed to save file"), QStringLiteral("%1\n\n%2").arg(QDir::toNativeSeparators(path), file.errorString()));
102+
return false;
103+
}
19104
return true;
20105
}
21106

src/plugins/midiformatconverter/internal/MIDIFileExporter.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
#ifndef MIDI_FORMAT_CONVERTER_MIDIFILEEXPORTER_H
2-
#define MIDI_FORMAT_CONVERTER_MIDIFILEEXPORTER_H
1+
#ifndef DIFFSCOPE_MIDI_FORMAT_CONVERTER_MIDIFILEEXPORTER_H
2+
#define DIFFSCOPE_MIDI_FORMAT_CONVERTER_MIDIFILEEXPORTER_H
33

44
#include <importexportmanager/FileConverter.h>
55

@@ -16,4 +16,4 @@ namespace MIDIFormatConverter::Internal {
1616

1717
}
1818

19-
#endif //MIDI_FORMAT_CONVERTER_MIDIFILEEXPORTER_H
19+
#endif //DIFFSCOPE_MIDI_FORMAT_CONVERTER_MIDIFILEEXPORTER_H

src/plugins/midiformatconverter/internal/MIDIFileImporter.cpp

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
#include "MIDIFileImporter.h"
22

3-
#include <QtCore/QtGlobal>
3+
#include <algorithm>
4+
#include <iterator>
5+
6+
#include <QLoggingCategory>
7+
#include <QFile>
8+
#include <QDir>
9+
10+
#include <CoreApi/runtimeinterface.h>
11+
12+
#include <SVSCraftCore/MusicPitch.h>
13+
#include <SVSCraftQuick/MessageBox.h>
14+
15+
#include <opendspx/model.h>
16+
#include <opendspxconverter/midi/midiconverter.h>
17+
#include <opendspxconverter/midi/midiintermediatedata.h>
18+
19+
#include <midiformatconverter/internal/MIDITrackSelectorDialog.h>
20+
#include <midiformatconverter/internal/MIDITextCodecConverter.h>
421

522
namespace MIDIFormatConverter::Internal {
623

24+
Q_STATIC_LOGGING_CATEGORY(lcMIDIFileImporter, "diffscope.midiformatconverter.midifileimporter")
25+
726
MIDIFileImporter::MIDIFileImporter(QObject *parent) : FileConverter(parent) {
827
setName(tr("MIDI"));
928
setDescription(tr("Import Standard MIDI file"));
@@ -14,8 +33,77 @@ namespace MIDIFormatConverter::Internal {
1433

1534
MIDIFileImporter::~MIDIFileImporter() = default;
1635

36+
static QList<MIDITrackSelectorDialog::TrackInfo> getTrackInfoList(const QList<QDspx::MidiIntermediateData::Track> &tracks) {
37+
QList<MIDITrackSelectorDialog::TrackInfo> result;
38+
for (const auto &track : tracks) {
39+
MIDITrackSelectorDialog::TrackInfo info;
40+
info.name = track.title;
41+
auto minMaxNotes = std::ranges::minmax_element(track.notes, [](const auto &a, const auto &b) { return a.key < b.key; });
42+
info.rangeText = track.notes.isEmpty() ? QString() : QStringLiteral("%1 - %2").arg(SVS::MusicPitch(static_cast<qint8>(minMaxNotes.min->key)).toString(SVS::MusicPitch::Flat), SVS::MusicPitch(static_cast<qint8>(minMaxNotes.max->key)).toString(SVS::MusicPitch::Flat));
43+
info.noteCount = static_cast<int>(track.notes.size());
44+
info.selectedByDefault = !track.notes.isEmpty();
45+
std::ranges::transform(track.notes, std::back_inserter(info.lyrics), [](const auto &note) { return note.lyric; });
46+
result.append(info);
47+
}
48+
return result;
49+
}
50+
1751
bool MIDIFileImporter::execImport(const QString &path, QDspx::Model &model, QWindow *window) {
18-
// TODO
52+
QFile file(path);
53+
if (!file.open(QIODevice::ReadOnly)) {
54+
qCCritical(lcMIDIFileImporter) << "Failed to read file:" << path << file.errorString();
55+
SVS::MessageBox::critical(Core::RuntimeInterface::qmlEngine(), window, tr("Failed to open file"), QStringLiteral("%1\n\n%2").arg(QDir::toNativeSeparators(path), file.errorString()));
56+
return false;
57+
}
58+
auto data = file.readAll();
59+
QDspx::MidiConverter::Error error;
60+
auto intermediateData = QDspx::MidiConverter::convertMidiToIntermediate(data, error);
61+
if (error == QDspx::MidiConverter::Error::InvalidMidiData) {
62+
qCCritical(lcMIDIFileImporter) << "Invalid MIDI data:" << path;
63+
SVS::MessageBox::critical(Core::RuntimeInterface::qmlEngine(), window, tr("Invalid MIDI data"), tr("The file is not a valid MIDI file"));
64+
return false;
65+
}
66+
if (error == QDspx::MidiConverter::Error::UnsupportedFormat) {
67+
qCCritical(lcMIDIFileImporter) << "Unsupported MIDI format:" << path;
68+
SVS::MessageBox::critical(Core::RuntimeInterface::qmlEngine(), window, tr("Unsupported MIDI format"), tr("The file is not a supported MIDI format"));
69+
return false;
70+
}
71+
MIDITrackSelectorDialog dlg;
72+
dlg.setTrackInfoList(getTrackInfoList(intermediateData.tracks()));
73+
dlg.detectCodec();
74+
connect(&dlg, &MIDITrackSelectorDialog::separateMidiChannelsChanged, [&](bool enabled) {
75+
auto intermediateData_ = QDspx::MidiConverter::convertMidiToIntermediate(data, error, { enabled });
76+
if (error == QDspx::MidiConverter::Error::NoError) {
77+
intermediateData = intermediateData_;
78+
dlg.setTrackInfoList(getTrackInfoList(intermediateData.tracks()));
79+
dlg.detectCodec();
80+
}
81+
});
82+
if (dlg.exec() != QDialog::Accepted) {
83+
return false;
84+
}
85+
auto codec = dlg.codec();
86+
auto selectedIndexes = dlg.selectedIndexes();
87+
bool ok;
88+
QList<QDspx::MidiIntermediateData::Track> selectedTracks;
89+
for (auto index : selectedIndexes) {
90+
selectedTracks.append(intermediateData.tracks().at(index));
91+
}
92+
intermediateData = {
93+
intermediateData.resolution(),
94+
dlg.importTempo() ? intermediateData.tempos() : QList<QDspx::MidiIntermediateData::Tempo>{},
95+
dlg.importTimeSignature() ? intermediateData.timeSignatures() : QList<QDspx::MidiIntermediateData::TimeSignature>{},
96+
intermediateData.markers(),
97+
selectedTracks
98+
};
99+
model = QDspx::MidiConverter::convertIntermediateToDspx(intermediateData, [codec](const QByteArray &text) {
100+
return MIDITextCodecConverter::decode(text, codec);
101+
}, &ok);
102+
if (!ok) {
103+
qCCritical(lcMIDIFileImporter) << "Failed to convert MIDI data:" << path;
104+
SVS::MessageBox::critical(Core::RuntimeInterface::qmlEngine(), window, tr("Failed to convert MIDI data"), tr("Some meta events in this MIDI document cannot be converted to DSPX. Please try disabling import tempo/time signature."));
105+
return false;
106+
}
19107
return true;
20108
}
21109

0 commit comments

Comments
 (0)