Skip to content

Commit 6fae757

Browse files
committed
Fix GitHub Issue #39: Save patch changes in midi export
- Optionally exports and imports program change and bank settings
1 parent 1ebc6a6 commit 6fae757

File tree

18 files changed

+531
-119
lines changed

18 files changed

+531
-119
lines changed

CHANGELOG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ New features:
1212

1313
* Make velocity interpolation dialog remember the previous values
1414

15+
* Fix GitHub Issue #39: Save patch changes in midi export
16+
- Optionally exports and imports program change and bank settings
17+
1518
Bug fixes:
1619

1720
* Fix crash when resizing after track deletion

src/application/application.cpp

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -312,14 +312,14 @@ void Application::connectApplicationService()
312312
connect(m_applicationService.get(), &ApplicationService::liveNoteOffRequested, m_midiService.get(), &MidiService::stopNote);
313313
connect(m_applicationService.get(), &ApplicationService::allNotesOffRequested, this, &Application::stopAllNotes);
314314

315-
connect(m_applicationService.get(), &ApplicationService::midiExportRequested, this, qOverload<QString, quint64, quint64>(&Application::exportToMidi));
315+
connect(m_applicationService.get(), &ApplicationService::midiExportRequested, this, &Application::exportToMidi);
316316
connect(m_applicationService.get(), &ApplicationService::midiImportRequested, this, &Application::importFromMidi);
317317
}
318318

319-
void Application::exportToMidi(QString fileName, quint64 startPosition, quint64 endPosition)
319+
void Application::exportToMidi(QString fileName, quint64 startPosition, quint64 endPosition, MidiExportOptions options)
320320
{
321321
try {
322-
m_midiExporter->exportTo(fileName.toStdString(), m_editorService->song(), startPosition, endPosition);
322+
m_midiExporter->exportTo(fileName.toStdString(), m_editorService->song(), startPosition, endPosition, options);
323323
const auto message = QString { "Exported the project to '%1' " }.arg(fileName);
324324
m_applicationService->requestStatusText(message);
325325
} catch (std::exception & e) {
@@ -328,12 +328,11 @@ void Application::exportToMidi(QString fileName, quint64 startPosition, quint64
328328
m_applicationService->requestStatusText(message);
329329
}
330330
}
331-
332-
void Application::importFromMidi(QString fileName, int importMode, int patternLength, bool quantizeNoteOn, bool quantizeNoteOff)
331+
void Application::importFromMidi(QString fileName, int importMode, int patternLength, bool quantizeNoteOn, bool quantizeNoteOff, bool connectMidiPorts)
333332
{
334333
try {
335334
const auto midiData = m_midiImporter->parseMidiFile(fileName.toStdString());
336-
m_midiImporter->importTo(midiData, m_editorService->song(), importMode, patternLength, quantizeNoteOn, quantizeNoteOff);
335+
m_midiImporter->importTo(midiData, m_editorService->song(), importMode, patternLength, quantizeNoteOn, quantizeNoteOff, connectMidiPorts ? m_midiService : nullptr);
337336
const auto message = QString { "Imported MIDI file '%1' " }.arg(fileName);
338337
m_applicationService->requestStatusText(message);
339338
m_editorService->setSong(m_editorService->song());

src/application/application.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ class Application : public QObject
115115
void requestInstruments(QStringList midiPorts);
116116
void stopAllNotes() const;
117117

118-
void exportToMidi(QString fileName, quint64 startPosition, quint64 endPosition);
119-
void importFromMidi(QString fileName, int importMode, int patternLength, bool quantizeNoteOn, bool quantizeNoteOff);
118+
void exportToMidi(QString fileName, quint64 startPosition, quint64 endPosition, MidiExportOptions options);
119+
void importFromMidi(QString fileName, int importMode, int patternLength, bool quantizeNoteOn, bool quantizeNoteOff, bool connectMidiPorts);
120120

121121
std::unique_ptr<UiLogger> m_uiLogger;
122122

src/application/service/application_service.cpp

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,17 @@ void ApplicationService::requestMidiExportDialog()
145145
emit midiExportDialogRequested();
146146
}
147147

148-
void ApplicationService::exportMidiFile(QUrl url, quint64 startPosition, quint64 endPosition)
148+
void ApplicationService::exportMidiFile(QUrl url, quint64 startPosition, quint64 endPosition, bool exportBank, bool exportProgramChange)
149149
{
150150
auto fileName = url.toLocalFile();
151151
juzzlin::L(TAG).info() << "MIDI export requested for " << fileName.toStdString() << " from " << startPosition << " to " << endPosition;
152152
if (!fileName.endsWith(Constants::midiFileExtension())) {
153153
fileName += Constants::midiFileExtension();
154154
}
155-
emit midiExportRequested(fileName, startPosition, endPosition);
155+
MidiExportOptions options;
156+
options.exportBank = exportBank;
157+
options.exportProgramChange = exportProgramChange;
158+
emit midiExportRequested(fileName, startPosition, endPosition, options);
156159
}
157160

158161
void ApplicationService::requestMidiImportDialog()
@@ -161,11 +164,11 @@ void ApplicationService::requestMidiImportDialog()
161164
emit midiImportDialogRequested();
162165
}
163166

164-
void ApplicationService::importMidiFile(QUrl url, int importMode, int patternLength, bool quantizeNoteOn, bool quantizeNoteOff)
167+
void ApplicationService::importMidiFile(QUrl url, int importMode, int patternLength, bool quantizeNoteOn, bool quantizeNoteOff, bool connectMidiPorts)
165168
{
166169
const auto fileName = url.toLocalFile();
167-
juzzlin::L(TAG).info() << "MIDI import requested for " << fileName.toStdString() << " mode: " << importMode << " length: " << patternLength << " quantizeNoteOn: " << quantizeNoteOn << " quantizeNoteOff: " << quantizeNoteOff;
168-
emit midiImportRequested(fileName, importMode, patternLength, quantizeNoteOn, quantizeNoteOff);
170+
juzzlin::L(TAG).info() << "MIDI import requested for " << fileName.toStdString() << " mode: " << importMode << " length: " << patternLength << " quantizeNoteOn: " << quantizeNoteOn << " quantizeNoteOff: " << quantizeNoteOff << " connectMidiPorts: " << connectMidiPorts;
171+
emit midiImportRequested(fileName, importMode, patternLength, quantizeNoteOn, quantizeNoteOff, connectMidiPorts);
169172
}
170173

171174
void ApplicationService::requestLiveNoteOn(quint8 key, quint8 octave, quint8 velocity)

src/application/service/application_service.hpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
#include <memory>
2525

26+
#include "../../infra/midi/export/midi_exporter.hpp"
27+
2628
namespace noteahead {
2729

2830
class EditorService;
@@ -89,10 +91,10 @@ class ApplicationService : public QObject
8991
Q_INVOKABLE void saveProjectAsTemplate(QUrl url);
9092

9193
Q_INVOKABLE void requestMidiExportDialog();
92-
Q_INVOKABLE void exportMidiFile(QUrl url, quint64 startPosition, quint64 endPosition);
94+
Q_INVOKABLE void exportMidiFile(QUrl url, quint64 startPosition, quint64 endPosition, bool exportBank, bool exportProgramChange);
9395

9496
Q_INVOKABLE void requestMidiImportDialog();
95-
Q_INVOKABLE void importMidiFile(QUrl url, int importMode, int patternLength, bool quantizeNoteOn, bool quantizeNoteOff);
97+
Q_INVOKABLE void importMidiFile(QUrl url, int importMode, int patternLength, bool quantizeNoteOn, bool quantizeNoteOff, bool connectMidiPorts);
9698

9799
Q_INVOKABLE virtual bool editMode() const;
98100
Q_INVOKABLE virtual void setEditMode(bool editMode);
@@ -142,10 +144,10 @@ class ApplicationService : public QObject
142144
void saveAsDialogRequested();
143145
void saveAsTemplateDialogRequested();
144146
void midiExportDialogRequested();
145-
void midiExportRequested(QString fileName, quint64 startPosition, quint64 endPosition);
147+
void midiExportRequested(QString fileName, quint64 startPosition, quint64 endPosition, MidiExportOptions options);
146148

147149
void midiImportDialogRequested();
148-
void midiImportRequested(QString fileName, int importMode, int patternLength, bool quantizeNoteOn, bool quantizeNoteOff);
150+
void midiImportRequested(QString fileName, int importMode, int patternLength, bool quantizeNoteOn, bool quantizeNoteOff, bool connectMidiPorts);
149151

150152
void alertDialogRequested(QString message);
151153
void statusTextRequested(QString message);

src/application/service/midi_service.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class MidiService : public QObject
4343

4444
~MidiService() override;
4545

46-
Q_INVOKABLE QStringList outputPorts() const;
46+
virtual Q_INVOKABLE QStringList outputPorts() const;
4747

4848
// QML API
4949
Q_INVOKABLE void setIsPlaying(bool isPlaying);

src/infra/midi/export/midi_exporter.cpp

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,18 @@ namespace {
4242
constexpr uint8_t MIDI_PORT_EVENT = 0x21;
4343
constexpr uint8_t TRACK_NAME_EVENT = 0x03;
4444
constexpr uint8_t END_OF_TRACK_EVENT = 0x2f;
45+
constexpr uint8_t DEVICE_NAME_EVENT = 0x09;
4546
constexpr uint8_t SET_TEMPO_EVENT = 0x51;
4647

4748
constexpr uint8_t NOTE_ON_STATUS = 0x90;
4849
constexpr uint8_t NOTE_OFF_STATUS = 0x80;
4950
constexpr uint8_t CONTROL_CHANGE_STATUS = 0xb0;
51+
constexpr uint8_t PROGRAM_CHANGE_STATUS = 0xc0;
5052
constexpr uint8_t PITCH_BEND_STATUS = 0xe0;
5153

54+
constexpr uint8_t BANK_SELECT_MSB = 0x00;
55+
constexpr uint8_t BANK_SELECT_LSB = 0x20;
56+
5257
void writeBeU32(std::ostream & out, uint32_t value)
5358
{
5459
out.put(static_cast<uint8_t>(value >> 24) & 0xff);
@@ -95,7 +100,7 @@ void MidiExporter::sortEvents(std::vector<EventS> & events)
95100
});
96101
}
97102

98-
void MidiExporter::exportTo(std::string fileName, SongW songW, size_t startPosition, size_t endPosition) const
103+
void MidiExporter::exportTo(std::string fileName, SongW songW, size_t startPosition, size_t endPosition, MidiExportOptions options) const
99104
{
100105
auto song = songW.lock();
101106
if (!song) {
@@ -127,8 +132,8 @@ void MidiExporter::exportTo(std::string fileName, SongW songW, size_t startPosit
127132

128133
auto filteredEvents = filterEvents(renderedEvents, m_mixerService);
129134

130-
const auto activeTracks = discoverActiveTracks(song, filteredEvents);
131-
const auto trackData = buildTrackData(song, filteredEvents, activeTracks);
135+
const auto activeTracks = discoverActiveTracks(song, filteredEvents, options);
136+
const auto trackData = buildTrackData(song, filteredEvents, activeTracks, options);
132137

133138
writeMidiHeader(out, song, activeTracks.trackToInstrument.size());
134139
writeTempoTrack(out, song);
@@ -169,7 +174,7 @@ void MidiExporter::writeMidiHeader(std::ostream & out, const SongS & song, size_
169174
writeBeU16(out, ticksPerQuarterNote);
170175
}
171176

172-
MidiExporter::ActiveTracks MidiExporter::discoverActiveTracks(const SongS & song, const std::vector<EventS> & events) const
177+
MidiExporter::ActiveTracks MidiExporter::discoverActiveTracks(const SongS & song, const std::vector<EventS> & events, MidiExportOptions options) const
173178
{
174179
ActiveTracks activeTracks;
175180
std::set<std::string> portNames;
@@ -185,6 +190,18 @@ MidiExporter::ActiveTracks MidiExporter::discoverActiveTracks(const SongS & song
185190
}
186191
}
187192

193+
// Also add tracks that have Bank or Program settings if enabled
194+
for (size_t i = 0; i < song->trackCount(); ++i) {
195+
if (auto instrument = song->instrument(i)) {
196+
const auto & settings = instrument->settings();
197+
const bool hasBank = options.exportBank && settings.bank.has_value();
198+
const bool hasPatch = options.exportProgramChange && settings.patch.has_value();
199+
if (hasBank || hasPatch) {
200+
activeTrackIndices.insert(i);
201+
}
202+
}
203+
}
204+
188205
for (const auto & trackIndex : activeTrackIndices) {
189206
auto instrument = song->instrument(trackIndex);
190207
if (!instrument) {
@@ -208,7 +225,7 @@ MidiExporter::ActiveTracks MidiExporter::discoverActiveTracks(const SongS & song
208225
return activeTracks;
209226
}
210227

211-
MidiExporter::ByteVector MidiExporter::initializeTrack(const SongS & song, size_t trackIndex, const ActiveTracks & activeTracks) const
228+
MidiExporter::ByteVector MidiExporter::initializeTrack(const SongS & song, size_t trackIndex, uint8_t channel, const ActiveTracks & activeTracks, MidiExportOptions options) const
212229
{
213230
ByteVector data;
214231

@@ -223,6 +240,13 @@ MidiExporter::ByteVector MidiExporter::initializeTrack(const SongS & song, size_
223240
data.push_back(static_cast<char>(portIndex));
224241
juzzlin::L(TAG).info() << "Writing MIDI Port Meta-Event for track " << trackIndex << ", portIndex: " << static_cast<int>(portIndex);
225242

243+
// Add Device Name Meta-Event (Port Name)
244+
data.push_back(static_cast<char>(0x00)); // Delta time
245+
data.push_back(static_cast<char>(META_EVENT));
246+
data.push_back(static_cast<char>(DEVICE_NAME_EVENT));
247+
writeVlq(data, static_cast<uint32_t>(portName.length()));
248+
data.insert(data.end(), portName.begin(), portName.end());
249+
226250
// Add Track Name Meta-Event
227251
data.push_back(static_cast<char>(0x00)); // Delta time
228252
data.push_back(static_cast<char>(META_EVENT));
@@ -231,6 +255,27 @@ MidiExporter::ByteVector MidiExporter::initializeTrack(const SongS & song, size_
231255
writeVlq(data, static_cast<uint32_t>(trackName.length()));
232256
data.insert(data.end(), trackName.begin(), trackName.end());
233257

258+
const auto instrument = activeTracks.trackToInstrument.at(trackIndex);
259+
const auto & settings = instrument->settings();
260+
261+
if (options.exportBank && settings.bank.has_value()) {
262+
data.push_back(static_cast<char>(0x00)); // Delta time
263+
data.push_back(static_cast<char>(CONTROL_CHANGE_STATUS | channel));
264+
data.push_back(static_cast<char>(BANK_SELECT_MSB));
265+
data.push_back(static_cast<char>(settings.bank->msb));
266+
267+
data.push_back(static_cast<char>(0x00)); // Delta time
268+
data.push_back(static_cast<char>(CONTROL_CHANGE_STATUS | channel));
269+
data.push_back(static_cast<char>(BANK_SELECT_LSB));
270+
data.push_back(static_cast<char>(settings.bank->lsb));
271+
}
272+
273+
if (options.exportProgramChange && settings.patch.has_value()) {
274+
data.push_back(static_cast<char>(0x00)); // Delta time
275+
data.push_back(static_cast<char>(PROGRAM_CHANGE_STATUS | channel));
276+
data.push_back(static_cast<char>(*settings.patch));
277+
}
278+
234279
return data;
235280
}
236281

@@ -266,14 +311,14 @@ void MidiExporter::writePitchBendEvent(ByteVector & dataOut, uint8_t channel, co
266311
dataOut.push_back(static_cast<char>(pitchBendData.msb()));
267312
}
268313

269-
std::map<size_t, MidiExporter::ByteVector> MidiExporter::buildTrackData(const SongS & song, const std::vector<EventS> & events, const ActiveTracks & activeTracks) const
314+
std::map<size_t, MidiExporter::ByteVector> MidiExporter::buildTrackData(const SongS & song, const std::vector<EventS> & events, const ActiveTracks & activeTracks, MidiExportOptions options) const
270315
{
271-
auto initialState = initializeTracks(song, activeTracks);
316+
auto initialState = initializeTracks(song, activeTracks, options);
272317
auto processedState = processEvents(std::move(initialState), events);
273318
return finalizeTracks(std::move(processedState));
274319
}
275320

276-
MidiExporter::TrackProcessingState MidiExporter::initializeTracks(const SongS & song, const ActiveTracks & activeTracks) const
321+
MidiExporter::TrackProcessingState MidiExporter::initializeTracks(const SongS & song, const ActiveTracks & activeTracks, MidiExportOptions options) const
277322
{
278323
TrackProcessingState state;
279324
std::map<std::string, uint8_t> portChannelCounters;
@@ -286,9 +331,10 @@ MidiExporter::TrackProcessingState MidiExporter::initializeTracks(const SongS &
286331
if (channel == 9) { // Skip drum channel 10
287332
channel++;
288333
}
289-
state.trackToChannelMap[trackIndex] = channel++;
334+
const uint8_t trackChannel = channel++;
335+
state.trackToChannelMap[trackIndex] = trackChannel;
290336

291-
state.allTracksData[trackIndex] = initializeTrack(song, trackIndex, activeTracks);
337+
state.allTracksData[trackIndex] = initializeTrack(song, trackIndex, trackChannel, activeTracks, options);
292338
}
293339
return state;
294340
}

src/infra/midi/export/midi_exporter.hpp

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ class PitchBendData;
3636
class SideChainService;
3737
class Song;
3838

39+
struct MidiExportOptions {
40+
bool exportBank = true;
41+
bool exportProgramChange = true;
42+
};
43+
3944
class MidiExporter {
4045
public:
4146
using AutomationServiceS = std::shared_ptr<AutomationService>;
@@ -55,7 +60,7 @@ class MidiExporter {
5560

5661
MidiExporter(AutomationServiceS automationService, MixerServiceS mixerService, SideChainServiceS sidechainService);
5762

58-
void exportTo(std::string fileName, SongW songW, size_t startPosition = 0, size_t endPosition = std::numeric_limits<size_t>::max()) const;
63+
void exportTo(std::string fileName, SongW songW, size_t startPosition = 0, size_t endPosition = std::numeric_limits<size_t>::max(), MidiExportOptions options = MidiExportOptions()) const;
5964

6065
private:
6166
struct TrackProcessingState {
@@ -65,16 +70,15 @@ class MidiExporter {
6570
};
6671

6772
void writeMidiHeader(std::ostream & out, const SongS & song, size_t numNoteTracks) const;
68-
ActiveTracks discoverActiveTracks(const SongS& song, const std::vector<EventS> & events) const;
69-
73+
ActiveTracks discoverActiveTracks(const SongS & song, const std::vector<EventS> & events, MidiExportOptions options) const;
7074
std::vector<EventS> filterEvents(const std::vector<EventS> & events, MixerServiceS mixerService) const;
7175

72-
std::map<size_t, ByteVector> buildTrackData(const SongS& song, const std::vector<EventS> & events, const ActiveTracks & activeTracks) const;
73-
TrackProcessingState initializeTracks(const SongS& song, const ActiveTracks & activeTracks) const;
76+
std::map<size_t, ByteVector> buildTrackData(const SongS& song, const std::vector<EventS> & events, const ActiveTracks & activeTracks, MidiExportOptions options) const;
77+
TrackProcessingState initializeTracks(const SongS& song, const ActiveTracks & activeTracks, MidiExportOptions options) const;
7478
TrackProcessingState processEvents(TrackProcessingState state, const std::vector<EventS> & events) const;
7579
std::map<size_t, ByteVector> finalizeTracks(TrackProcessingState state) const;
7680

77-
ByteVector initializeTrack(const SongS& song, size_t trackIndex, const ActiveTracks & activeTracks) const;
81+
ByteVector initializeTrack(const SongS& song, size_t trackIndex, uint8_t channel, const ActiveTracks & activeTracks, MidiExportOptions options) const;
7882
void writeNoteOnEvent(ByteVector & data, uint8_t channel, const NoteData & noteData) const;
7983
void writeNoteOffEvent(ByteVector & data, uint8_t channel, const NoteData & noteData) const;
8084
void writeControlChangeEvent(ByteVector & data, uint8_t channel, const MidiCcData & ccData) const;

0 commit comments

Comments
 (0)