mirror of
https://github.com/LMMS/lmms.git
synced 2025-12-23 22:58:33 -05:00
Instead of including the files directly in plugins/MidiImport/portsmf, that path is now a git submodule (https://github.com/portsmf/portsmf). Several other changes have been made to clean up MidiImport as well.
592 lines
15 KiB
C++
592 lines
15 KiB
C++
/*
|
|
* MidiImport.cpp - support for importing MIDI files
|
|
*
|
|
* Copyright (c) 2005-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
|
|
*
|
|
* This file is part of LMMS - https://lmms.io
|
|
*
|
|
* 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.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public
|
|
* License along with this program (see COPYING); if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301 USA.
|
|
*
|
|
*/
|
|
|
|
|
|
#include <QDir>
|
|
#include <QApplication>
|
|
#include <QFile>
|
|
#include <QMessageBox>
|
|
#include <QProgressDialog>
|
|
|
|
#include <sstream>
|
|
#include <unordered_map>
|
|
|
|
#include "MidiImport.h"
|
|
#include "TrackContainer.h"
|
|
#include "InstrumentTrack.h"
|
|
#include "AutomationTrack.h"
|
|
#include "AutomationClip.h"
|
|
#include "ConfigManager.h"
|
|
#include "MidiClip.h"
|
|
#include "Instrument.h"
|
|
#include "GuiApplication.h"
|
|
#include "MainWindow.h"
|
|
#include "TimePos.h"
|
|
#include "Song.h"
|
|
|
|
#include "plugin_export.h"
|
|
|
|
#include "portsmf/include/allegro.h"
|
|
|
|
namespace
|
|
{
|
|
constexpr std::int32_t makeID(const char c[4]) { return c[0] | (c[1] << 8) | (c[2] << 16) | (c[3] << 24); }
|
|
}
|
|
|
|
namespace lmms
|
|
{
|
|
|
|
|
|
extern "C"
|
|
{
|
|
|
|
Plugin::Descriptor PLUGIN_EXPORT midiimport_plugin_descriptor =
|
|
{
|
|
LMMS_STRINGIFY(PLUGIN_NAME),
|
|
"MIDI Import",
|
|
QT_TRANSLATE_NOOP("PluginBrowser", "Filter for importing MIDI-files into LMMS"),
|
|
"Tobias Doerffel <tobydox/at/users/dot/sf/dot/net>",
|
|
0x0100,
|
|
Plugin::Type::ImportFilter,
|
|
nullptr,
|
|
nullptr,
|
|
nullptr,
|
|
};
|
|
|
|
PLUGIN_EXPORT Plugin* lmms_plugin_main(Model*, void* data)
|
|
{
|
|
return new MidiImport(QString::fromUtf8(static_cast<const char*>(data)));
|
|
}
|
|
|
|
}
|
|
|
|
|
|
MidiImport::MidiImport(const QString& file) :
|
|
ImportFilter(file, &midiimport_plugin_descriptor)
|
|
{}
|
|
|
|
|
|
bool MidiImport::tryImport(TrackContainer* tc)
|
|
{
|
|
if (!openFile()) { return false; }
|
|
|
|
#ifdef LMMS_HAVE_FLUIDSYNTH
|
|
if (gui::getGUI() != nullptr && ConfigManager::inst()->sf2File().isEmpty())
|
|
{
|
|
QMessageBox::information(gui::getGUI()->mainWindow(),
|
|
tr("Setup incomplete"),
|
|
tr("You have not set up a default soundfont in "
|
|
"the settings dialog (Edit->Settings). "
|
|
"Therefore no sound will be played back after "
|
|
"importing this MIDI file. You should download "
|
|
"a General MIDI soundfont, specify it in "
|
|
"settings dialog and try again."));
|
|
}
|
|
#else
|
|
if (gui::getGUI() != nullptr)
|
|
{
|
|
QMessageBox::information(gui::getGUI()->mainWindow(),
|
|
tr("Setup incomplete"),
|
|
tr("You did not compile LMMS with support for "
|
|
"SoundFont2 player, which is used to add default "
|
|
"sound to imported MIDI files. "
|
|
"Therefore no sound will be played back after "
|
|
"importing this MIDI file."));
|
|
}
|
|
#endif
|
|
|
|
switch (read32LE()) // Read ID
|
|
{
|
|
case makeID("MThd"):
|
|
// printf("MidiImport::tryImport(): found MThd\n");
|
|
return readSMF(tc);
|
|
|
|
case makeID("RIFF"):
|
|
// printf("MidiImport::tryImport(): found RIFF\n");
|
|
return readRIFF(tc);
|
|
|
|
default:
|
|
printf("MidiImport::tryImport(): not a Standard MIDI file\n");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
class smfMidiCC
|
|
{
|
|
|
|
public:
|
|
AutomationTrack* at = nullptr;
|
|
AutomationClip* ap = nullptr;
|
|
TimePos lastPos = 0;
|
|
|
|
smfMidiCC& create(TrackContainer* tc, QString tn)
|
|
{
|
|
if (!at)
|
|
{
|
|
// Keep LMMS responsive, for now the import runs
|
|
// in the main thread. This should probably be
|
|
// removed if that ever changes.
|
|
qApp->processEvents();
|
|
at = dynamic_cast<AutomationTrack*>(Track::create(Track::Type::Automation, tc));
|
|
}
|
|
if (tn != "") { at->setName(tn); }
|
|
return *this;
|
|
}
|
|
|
|
|
|
void clear()
|
|
{
|
|
at = nullptr;
|
|
ap = nullptr;
|
|
lastPos = 0;
|
|
}
|
|
|
|
|
|
smfMidiCC& putValue(TimePos time, AutomatableModel* objModel, float value)
|
|
{
|
|
if (!ap || time > lastPos + DefaultTicksPerBar)
|
|
{
|
|
TimePos pPos = TimePos(time.getBar(), 0);
|
|
ap = dynamic_cast<AutomationClip*>(at->createClip(pPos));
|
|
ap->addObject(objModel);
|
|
}
|
|
|
|
lastPos = time;
|
|
time = time - ap->startPosition();
|
|
ap->putValue(time, value, false);
|
|
ap->changeLength(TimePos(time.getBar() + 1, 0));
|
|
return *this;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
class smfMidiChannel
|
|
{
|
|
|
|
public:
|
|
InstrumentTrack* it = nullptr;
|
|
MidiClip* p = nullptr;
|
|
Instrument* it_inst = nullptr;
|
|
bool isSF2 = false;
|
|
bool hasNotes = false;
|
|
QString trackName;
|
|
|
|
smfMidiChannel* create(TrackContainer* tc, QString tn)
|
|
{
|
|
if (!it) {
|
|
// Keep LMMS responsive
|
|
qApp->processEvents();
|
|
it = dynamic_cast<InstrumentTrack*>(Track::create(Track::Type::Instrument, tc));
|
|
|
|
#ifdef LMMS_HAVE_FLUIDSYNTH
|
|
it_inst = it->loadInstrument("sf2player");
|
|
|
|
if (it_inst)
|
|
{
|
|
isSF2 = true;
|
|
it_inst->loadFile(ConfigManager::inst()->sf2File());
|
|
it_inst->childModel("bank")->setValue(0);
|
|
it_inst->childModel("patch")->setValue(0);
|
|
}
|
|
else { it_inst = it->loadInstrument("patman"); }
|
|
#else
|
|
it_inst = it->loadInstrument("patman");
|
|
#endif
|
|
trackName = tn;
|
|
if (trackName != "") { it->setName(tn); }
|
|
// General MIDI default
|
|
it->pitchRangeModel()->setInitValue(2);
|
|
// Create a default pattern
|
|
p = dynamic_cast<MidiClip*>(it->createClip(0));
|
|
}
|
|
return this;
|
|
}
|
|
|
|
|
|
void addNote(Note& n)
|
|
{
|
|
if (!p) { p = dynamic_cast<MidiClip*>(it->createClip(0)); }
|
|
p->addNote(n, false);
|
|
hasNotes = true;
|
|
}
|
|
|
|
|
|
void splitMidiClips()
|
|
{
|
|
MidiClip* newMidiClip = nullptr;
|
|
TimePos lastEnd(0);
|
|
|
|
p->rearrangeAllNotes();
|
|
for (auto n : p->notes())
|
|
{
|
|
if (!newMidiClip || n->pos() > lastEnd + DefaultTicksPerBar)
|
|
{
|
|
TimePos pPos = TimePos(n->pos().getBar(), 0);
|
|
newMidiClip = dynamic_cast<MidiClip*>(it->createClip(pPos));
|
|
}
|
|
lastEnd = n->pos() + n->length();
|
|
|
|
Note newNote(*n);
|
|
newNote.setPos(n->pos(newMidiClip->startPosition()));
|
|
newMidiClip->addNote(newNote, false);
|
|
}
|
|
|
|
delete p;
|
|
p = nullptr;
|
|
}
|
|
};
|
|
|
|
|
|
bool MidiImport::readSMF(TrackContainer* tc)
|
|
{
|
|
constexpr int MIDI_CC_COUNT = 128 + 1; // 0-127 (128) + pitch bend
|
|
constexpr int preTrackSteps = 2;
|
|
QProgressDialog pd(TrackContainer::tr("Importing MIDI-file..."),
|
|
TrackContainer::tr("Cancel"), 0, preTrackSteps, gui::getGUI()->mainWindow());
|
|
pd.setWindowTitle(TrackContainer::tr("Please wait..."));
|
|
pd.setWindowModality(Qt::WindowModal);
|
|
pd.setMinimumDuration(0);
|
|
|
|
pd.setValue(0);
|
|
|
|
std::istringstream stream(readAllData().toStdString());
|
|
auto seq = new Alg_seq(stream, true);
|
|
seq->convert_to_beats();
|
|
|
|
pd.setMaximum(seq->tracks() + preTrackSteps);
|
|
pd.setValue(1);
|
|
|
|
// 128 CC + Pitch Bend
|
|
auto ccs = std::array<smfMidiCC, MIDI_CC_COUNT>{};
|
|
|
|
// channel to CC object for program changes
|
|
std::unordered_map<long, smfMidiCC> pcs;
|
|
|
|
// channels can be set out of 256 range
|
|
// using unordered_map should fix most invalid loads and crashes while loading
|
|
std::unordered_map<long, smfMidiChannel> chs;
|
|
// NOTE: unordered_map::operator[] creates a new element if none exists
|
|
|
|
MeterModel & timeSigMM = Engine::getSong()->getTimeSigModel();
|
|
auto nt = dynamic_cast<AutomationTrack*>(Track::create(Track::Type::Automation, Engine::getSong()));
|
|
nt->setName(tr("MIDI Time Signature Numerator"));
|
|
auto dt = dynamic_cast<AutomationTrack*>(Track::create(Track::Type::Automation, Engine::getSong()));
|
|
dt->setName(tr("MIDI Time Signature Denominator"));
|
|
auto timeSigNumeratorPat = new AutomationClip(nt);
|
|
timeSigNumeratorPat->setDisplayName(tr("Numerator"));
|
|
timeSigNumeratorPat->addObject(&timeSigMM.numeratorModel());
|
|
auto timeSigDenominatorPat = new AutomationClip(dt);
|
|
timeSigDenominatorPat->setDisplayName(tr("Denominator"));
|
|
timeSigDenominatorPat->addObject(&timeSigMM.denominatorModel());
|
|
|
|
// TODO: adjust these to Time.Sig changes
|
|
double beatsPerBar = 4;
|
|
double ticksPerBeat = DefaultTicksPerBar / beatsPerBar;
|
|
|
|
// Time-sig changes
|
|
Alg_time_sigs* timeSigs = &seq->time_sig;
|
|
for (int s = 0; s < timeSigs->length(); ++s)
|
|
{
|
|
Alg_time_sig timeSig = (*timeSigs)[s];
|
|
timeSigNumeratorPat->putValue(timeSig.beat * ticksPerBeat, timeSig.num);
|
|
timeSigDenominatorPat->putValue(timeSig.beat * ticksPerBeat, timeSig.den);
|
|
}
|
|
// manually call otherwise the pattern shows being 1 bar
|
|
timeSigNumeratorPat->updateLength();
|
|
timeSigDenominatorPat->updateLength();
|
|
|
|
pd.setValue(2);
|
|
|
|
// Tempo stuff
|
|
auto tt = dynamic_cast<AutomationTrack*>(Track::create(Track::Type::Automation, Engine::getSong()));
|
|
tt->setName(tr("Tempo"));
|
|
auto tap = new AutomationClip(tt);
|
|
tap->setDisplayName(tr("Tempo"));
|
|
tap->addObject(&Engine::getSong()->tempoModel());
|
|
if (tap)
|
|
{
|
|
tap->clear();
|
|
Alg_time_map* timeMap = seq->get_time_map();
|
|
Alg_beats& beats = timeMap->beats;
|
|
for (int i = 0; i < beats.len - 1; ++i)
|
|
{
|
|
Alg_beat_ptr b = &(beats[i]);
|
|
double tempo = (beats[i + 1].beat - b->beat) / (beats[i + 1].time - beats[i].time);
|
|
tap->putValue(b->beat * ticksPerBeat, tempo * 60.0);
|
|
}
|
|
if (timeMap->last_tempo_flag)
|
|
{
|
|
Alg_beat_ptr b = &beats[beats.len - 1];
|
|
tap->putValue(b->beat * ticksPerBeat, timeMap->last_tempo * 60.0);
|
|
}
|
|
}
|
|
|
|
// Update the tempo to avoid crash when playing a project imported
|
|
// via the command line
|
|
Engine::updateFramesPerTick();
|
|
|
|
// Song events
|
|
for (int e = 0; e < seq->length(); ++e)
|
|
{
|
|
Alg_event_ptr evt = (*seq)[e];
|
|
|
|
if (evt->is_update())
|
|
{
|
|
printf("Unhandled SONG update: %d %f %s\n",
|
|
evt->get_type_code(), evt->time, evt->get_attribute());
|
|
}
|
|
}
|
|
|
|
// Tracks
|
|
for (int t = 0; t < seq->tracks(); ++t)
|
|
{
|
|
QString trackName = QString(tr("Track") + " %1").arg(t);
|
|
Alg_track_ptr trk = seq->track(t);
|
|
pd.setValue(t + preTrackSteps);
|
|
|
|
for (auto& cc : ccs) { cc.clear(); }
|
|
|
|
// Now look at events
|
|
for (int e = 0; e < trk->length(); ++e)
|
|
{
|
|
Alg_event_ptr evt = (*trk)[e];
|
|
|
|
if (evt->chan == -1)
|
|
{
|
|
bool handled = false;
|
|
if (evt->is_update())
|
|
{
|
|
QString attr = evt->get_attribute();
|
|
// seqnames is a track0 identifier (see allegro code)
|
|
if (attr == (t == 0 ? "seqnames" : "tracknames")
|
|
&& evt->get_update_type() == 's')
|
|
{
|
|
trackName = evt->get_string_value();
|
|
handled = true;
|
|
}
|
|
}
|
|
if (!handled) {
|
|
// Write debug output
|
|
printf("MISSING GLOBAL HANDLER\n");
|
|
printf("\tChn: %ld, Type Code: %d, Time: %f", evt->chan, evt->get_type_code(), evt->time);
|
|
if (evt->is_update())
|
|
{
|
|
printf(", Update Type: %s", evt->get_attribute());
|
|
if (evt->get_update_type() == 'a') {
|
|
printf(", Atom: %s", evt->get_atom_value());
|
|
}
|
|
}
|
|
printf("\n");
|
|
}
|
|
}
|
|
else if (evt->is_note())
|
|
{
|
|
smfMidiChannel* ch = chs[evt->chan].create(tc, trackName);
|
|
auto noteEvt = dynamic_cast<Alg_note_ptr>(evt);
|
|
tick_t ticks = noteEvt->get_duration() * ticksPerBeat;
|
|
Note n(
|
|
ticks < 1 ? 1 : ticks,
|
|
noteEvt->get_start_time() * ticksPerBeat,
|
|
noteEvt->get_identifier(),
|
|
// Map from MIDI velocity to LMMS volume
|
|
noteEvt->get_loud() * (200.f / 127.f)
|
|
);
|
|
ch->addNote(n);
|
|
}
|
|
else if (evt->is_update())
|
|
{
|
|
smfMidiChannel* ch = chs[evt->chan].create(tc, trackName);
|
|
|
|
double time = evt->time*ticksPerBeat;
|
|
QString update(evt->get_attribute());
|
|
|
|
if (update == "programi")
|
|
{
|
|
const auto prog = evt->get_integer_value();
|
|
if (ch->isSF2)
|
|
{
|
|
auto& pc = pcs[evt->chan];
|
|
AutomatableModel* objModel = ch->it_inst->childModel("patch");
|
|
if (pc.at == nullptr) {
|
|
pc.create(tc, trackName + " > " + objModel->displayName());
|
|
}
|
|
pc.putValue(time, objModel, prog);
|
|
}
|
|
else
|
|
{
|
|
const QString num = QString::number(prog);
|
|
const QString filter = QString().fill('0', 3 - num.length()) + num + "*.pat";
|
|
const QString dir = "/usr/share/midi/"
|
|
"freepats/Tone_000/";
|
|
const QStringList files = QDir(dir).
|
|
entryList(QStringList(filter));
|
|
if (ch->it_inst && !files.empty())
|
|
{
|
|
ch->it_inst->loadFile(dir + files.front());
|
|
}
|
|
}
|
|
}
|
|
else if (update.startsWith("control") || update == "bendr")
|
|
{
|
|
int ccid = update.mid(7, update.length() - 8).toInt();
|
|
if (update == "bendr") { ccid = 128; }
|
|
if (ccid <= 128)
|
|
{
|
|
double cc = evt->get_real_value();
|
|
AutomatableModel* objModel = nullptr;
|
|
|
|
switch (ccid)
|
|
{
|
|
case 0:
|
|
if (ch->isSF2 && ch->it_inst)
|
|
{
|
|
objModel = ch->it_inst->childModel("bank");
|
|
printf("BANK SELECT %f %d\n", cc, static_cast<int>(cc * 127));
|
|
cc *= 127.0f;
|
|
}
|
|
break;
|
|
|
|
case 7:
|
|
objModel = ch->it->volumeModel();
|
|
cc *= 100.0f;
|
|
break;
|
|
|
|
case 10:
|
|
objModel = ch->it->panningModel();
|
|
cc = cc * 200.f - 100.0f;
|
|
break;
|
|
|
|
case 128:
|
|
objModel = ch->it->pitchModel();
|
|
cc = cc * 100.0f;
|
|
break;
|
|
|
|
default:
|
|
//TODO: something useful for other CCs
|
|
break;
|
|
}
|
|
|
|
if (objModel)
|
|
{
|
|
if (time == 0 && objModel)
|
|
{
|
|
objModel->setInitValue(cc);
|
|
}
|
|
else
|
|
{
|
|
if (ccs[ccid].at == nullptr) {
|
|
ccs[ccid].create(tc, trackName + " > " +
|
|
// This is inside if (objModel), so objModel should never be nullptr
|
|
// (objModel != nullptr ? objModel->displayName() : QString("CC %1").arg(ccid))
|
|
objModel->displayName()
|
|
);
|
|
}
|
|
ccs[ccid].putValue(time, objModel, cc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
printf("Unhandled update: %ld %d %f %s\n",
|
|
evt->chan, evt->get_type_code(), evt->time, evt->get_attribute());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
delete seq;
|
|
|
|
for (auto& c: chs)
|
|
{
|
|
if (c.second.hasNotes)
|
|
{
|
|
c.second.splitMidiClips();
|
|
}
|
|
else if (c.second.it)
|
|
{
|
|
printf(" Should remove empty track\n");
|
|
// must delete trackView first - but where is it?
|
|
//tc->removeTrack(chs[c].it);
|
|
//it->deleteLater();
|
|
}
|
|
// Set channel 10 to drums as per General MIDI's orders
|
|
if (c.first % 16l == 9 /* channel 10 */
|
|
&& c.second.hasNotes && c.second.it_inst && c.second.isSF2)
|
|
{
|
|
c.second.it_inst->childModel("bank")->setValue(128);
|
|
c.second.it_inst->childModel("patch")->setValue(0);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool MidiImport::readRIFF(TrackContainer* tc)
|
|
{
|
|
// skip file length
|
|
skip(4);
|
|
|
|
// check file type ("RMID" = RIFF MIDI)
|
|
if (read32LE() != makeID("RMID"))
|
|
{
|
|
invalid_format:
|
|
qWarning("MidiImport::readRIFF(): invalid file format");
|
|
return false;
|
|
}
|
|
|
|
// search for "data" chunk
|
|
while (true)
|
|
{
|
|
const std::int32_t id = read32LE();
|
|
const std::int32_t len = read32LE();
|
|
if (file().atEnd())
|
|
{
|
|
data_not_found:
|
|
qWarning("MidiImport::readRIFF(): data chunk not found");
|
|
return false;
|
|
}
|
|
if (id == makeID("data")) { break; }
|
|
if (len < 0) { goto data_not_found; }
|
|
skip((len + 1) & ~1);
|
|
}
|
|
|
|
// the "data" chunk must contain data in SMF format
|
|
if (read32LE() != makeID("MThd")) { goto invalid_format; }
|
|
return readSMF(tc);
|
|
}
|
|
|
|
|
|
void MidiImport::error()
|
|
{
|
|
printf("MidiImport::readTrack(): invalid MIDI data (offset %#x)\n",
|
|
static_cast<unsigned int>(file().pos()));
|
|
}
|
|
|
|
|
|
} // namespace lmms
|