view data/fileio/MIDIFileReader.cpp @ 1615:24dc8cb42755 single-point

Rename a number of classes and methods (including Point -> Event); comments
author Chris Cannam
date Thu, 07 Mar 2019 15:44:09 +0000
parents 70e172e6cc59
children 7a23dfe65d66
line wrap: on
line source
/* -*- 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.
*/


/*
   This is a modified version of a source file from the 
   Rosegarden MIDI and audio sequencer and notation editor.
   This file copyright 2000-2006 Richard Bown and Chris Cannam.
*/


#include <iostream>
#include <fstream>
#include <string>
#include <cstdio>
#include <algorithm>

#include "MIDIFileReader.h"

#include "data/midi/MIDIEvent.h"

#include "model/Model.h"
#include "base/Pitch.h"
#include "base/RealTime.h"
#include "model/NoteModel.h"

#include <QString>
#include <QFileInfo>

#include <sstream>

#include "base/Debug.h"

using std::string;
using std::ifstream;
using std::stringstream;
using std::ends;
using std::ios;
using std::vector;
using std::map;
using std::set;

using namespace MIDIConstants;

//#define MIDI_DEBUG 1


MIDIFileReader::MIDIFileReader(QString path,
                               MIDIFileImportPreferenceAcquirer *acquirer,
                               sv_samplerate_t mainModelSampleRate,
                               ProgressReporter *) : // we don't actually report progress
    m_smpte(false),
    m_timingDivision(0),
    m_fps(0),
    m_subframes(0),
    m_format(MIDI_FILE_BAD_FORMAT),
    m_numberOfTracks(0),
    m_trackByteCount(0),
    m_decrementCount(false),
    m_path(path),
    m_midiFile(nullptr),
    m_fileSize(0),
    m_mainModelSampleRate(mainModelSampleRate),
    m_acquirer(acquirer)
{
    if (parseFile()) {
        m_error = "";
    }
}

MIDIFileReader::~MIDIFileReader()
{
    for (MIDIComposition::iterator i = m_midiComposition.begin();
         i != m_midiComposition.end(); ++i) {
        
        for (MIDITrack::iterator j = i->second.begin();
             j != i->second.end(); ++j) {
            delete *j;
        }

        i->second.clear();
    }

    m_midiComposition.clear();
}

bool
MIDIFileReader::isOK() const
{
    return (m_error == "");
}

QString
MIDIFileReader::getError() const
{
    return m_error;
}

long
MIDIFileReader::midiBytesToLong(const string& bytes)
{
    if (bytes.length() != 4) {
        throw MIDIException(tr("Wrong length for long data in MIDI stream (%1, should be %2)").arg(bytes.length()).arg(4));
    }

    long longRet = ((long)(((MIDIByte)bytes[0]) << 24)) |
                   ((long)(((MIDIByte)bytes[1]) << 16)) |
                   ((long)(((MIDIByte)bytes[2]) << 8)) |
                   ((long)((MIDIByte)(bytes[3])));

    return longRet;
}

int
MIDIFileReader::midiBytesToInt(const string& bytes)
{
    if (bytes.length() != 2) {
        throw MIDIException(tr("Wrong length for int data in MIDI stream (%1, should be %2)").arg(bytes.length()).arg(2));
    }

    int intRet = ((int)(((MIDIByte)bytes[0]) << 8)) |
                 ((int)(((MIDIByte)bytes[1])));
    return(intRet);
}


// Gets a single byte from the MIDI byte stream.  For each track
// section we can read only a specified number of bytes held in
// m_trackByteCount.
//
MIDIByte
MIDIFileReader::getMIDIByte()
{
    if (!m_midiFile) {
        throw MIDIException(tr("getMIDIByte called but no MIDI file open"));
    }

    if (m_midiFile->eof()) {
        throw MIDIException(tr("End of MIDI file encountered while reading"));
    }

    if (m_decrementCount && m_trackByteCount <= 0) {
        throw MIDIException(tr("Attempt to get more bytes than expected on Track"));
    }

    char byte;
    if (m_midiFile->read(&byte, 1)) {
        --m_trackByteCount;
        return (MIDIByte)byte;
    }

    throw MIDIException(tr("Attempt to read past MIDI file end"));
}


// Gets a specified number of bytes from the MIDI byte stream.  For
// each track section we can read only a specified number of bytes
// held in m_trackByteCount.
//
string
MIDIFileReader::getMIDIBytes(unsigned long numberOfBytes)
{
    if (!m_midiFile) {
        throw MIDIException(tr("getMIDIBytes called but no MIDI file open"));
    }

    if (m_midiFile->eof()) {
        throw MIDIException(tr("End of MIDI file encountered while reading"));
    }

    if (m_decrementCount && (numberOfBytes > (unsigned long)m_trackByteCount)) {
        throw MIDIException(tr("Attempt to get more bytes than available on Track (%1, only have %2)").arg(numberOfBytes).arg(m_trackByteCount));
    }

    string stringRet;
    char fileMIDIByte;

    while (stringRet.length() < numberOfBytes &&
           m_midiFile->read(&fileMIDIByte, 1)) {
        stringRet += fileMIDIByte;
    }

    // if we've reached the end of file without fulfilling the
    // quota then panic as our parsing has performed incorrectly
    //
    if (stringRet.length() < numberOfBytes) {
        stringRet = "";
        throw MIDIException(tr("Attempt to read past MIDI file end"));
    }

    // decrement the byte count
    if (m_decrementCount)
        m_trackByteCount -= stringRet.length();

    return stringRet;
}


// Get a long number of variable length from the MIDI byte stream.
//
long
MIDIFileReader::getNumberFromMIDIBytes(int firstByte)
{
    if (!m_midiFile) {
        throw MIDIException(tr("getNumberFromMIDIBytes called but no MIDI file open"));
    }

    long longRet = 0;
    MIDIByte midiByte;

    if (firstByte >= 0) {
        midiByte = (MIDIByte)firstByte;
    } else if (m_midiFile->eof()) {
        return longRet;
    } else {
        midiByte = getMIDIByte();
    }

    longRet = midiByte;
    if (midiByte & 0x80) {
        longRet &= 0x7F;
        do {
            midiByte = getMIDIByte();
            longRet = (longRet << 7) + (midiByte & 0x7F);
        } while (!m_midiFile->eof() && (midiByte & 0x80));
    }

    return longRet;
}


// Seek to the next track in the midi file and set the number
// of bytes to be read in the counter m_trackByteCount.
//
bool
MIDIFileReader::skipToNextTrack()
{
    if (!m_midiFile) {
        throw MIDIException(tr("skipToNextTrack called but no MIDI file open"));
    }

    string buffer, buffer2;
    m_trackByteCount = -1;
    m_decrementCount = false;

    while (!m_midiFile->eof() && (m_decrementCount == false)) {
        buffer = getMIDIBytes(4); 
        if (buffer.compare(0, 4, MIDI_TRACK_HEADER) == 0) {
            m_trackByteCount = midiBytesToLong(getMIDIBytes(4));
            m_decrementCount = true;
        }
    }

    if (m_trackByteCount == -1) { // we haven't found a track
        return false;
    } else {
        return true;
    }
}


// Read in a MIDI file.  The parsing process throws exceptions back up
// here if we run into trouble which we can then pass back out to
// whoever called us using a nice bool.
//
bool
MIDIFileReader::parseFile()
{
    m_error = "";

#ifdef MIDI_DEBUG
    SVDEBUG << "MIDIFileReader::open() : fileName = " << m_fileName.c_str() << endl;
#endif

    // Open the file
    m_midiFile = new ifstream(m_path.toLocal8Bit().data(),
                              ios::in | ios::binary);

    if (!*m_midiFile) {
        m_error = "File not found or not readable.";
        m_format = MIDI_FILE_BAD_FORMAT;
        delete m_midiFile;
        m_midiFile = nullptr;
        return false;
    }

    bool retval = false;

    try {

        // Set file size so we can count it off
        //
        m_midiFile->seekg(0, ios::end);
        std::streamoff off = m_midiFile->tellg();
        m_fileSize = 0;
        if (off > 0) m_fileSize = off;
        m_midiFile->seekg(0, ios::beg);

        // Parse the MIDI header first.  The first 14 bytes of the file.
        if (!parseHeader(getMIDIBytes(14))) {
            m_format = MIDI_FILE_BAD_FORMAT;
            m_error = "Not a MIDI file.";
            goto done;
        }

        unsigned int i = 0;

        for (unsigned int j = 0; j < m_numberOfTracks; ++j) {

#ifdef MIDI_DEBUG
            SVDEBUG << "Parsing Track " << j << endl;
#endif

            if (!skipToNextTrack()) {
#ifdef MIDI_DEBUG
                SVDEBUG << "Couldn't find Track " << j << endl;
#endif
                m_error = "File corrupted or in non-standard format?";
                m_format = MIDI_FILE_BAD_FORMAT;
                goto done;
            }

#ifdef MIDI_DEBUG
            SVDEBUG << "Track has " << m_trackByteCount << " bytes" << endl;
#endif

            // Run through the events taking them into our internal
            // representation.
            if (!parseTrack(i)) {
#ifdef MIDI_DEBUG
                SVDEBUG << "Track " << j << " parsing failed" << endl;
#endif
                m_error = "File corrupted or in non-standard format?";
                m_format = MIDI_FILE_BAD_FORMAT;
                goto done;
            }

            ++i; // j is the source track number, i the destination
        }
        
        m_numberOfTracks = i;
        retval = true;

    } catch (const MIDIException &e) {

        SVDEBUG << "MIDIFileReader::open() - caught exception - " << e.what() << endl;
        m_error = e.what();
    }
    
done:
    m_midiFile->close();
    delete m_midiFile;

    for (unsigned int track = 0; track < m_numberOfTracks; ++track) {

        // Convert the deltaTime to an absolute time since the track
        // start.  The addTime method returns the sum of the current
        // MIDI Event delta time plus the argument.

        unsigned long acc = 0;

        for (MIDITrack::iterator i = m_midiComposition[track].begin();
             i != m_midiComposition[track].end(); ++i) {
            acc = (*i)->addTime(acc);
        }

        if (consolidateNoteOffEvents(track)) { // returns true if some notes exist
            m_loadableTracks.insert(track);
        }
    }

    for (unsigned int track = 0; track < m_numberOfTracks; ++track) {
        updateTempoMap(track);
    }

    calculateTempoTimestamps();

    return retval;
}

// Parse and ensure the MIDI Header is legitimate
//
bool
MIDIFileReader::parseHeader(const string &midiHeader)
{
    if (midiHeader.size() < 14) {
#ifdef MIDI_DEBUG
        SVDEBUG << "MIDIFileReader::parseHeader() - file header undersized" << endl;
#endif
        return false;
    }

    if (midiHeader.compare(0, 4, MIDI_FILE_HEADER) != 0) {
#ifdef MIDI_DEBUG
        SVDEBUG << "MIDIFileReader::parseHeader()"
             << "- file header not found or malformed"
             << endl;
#endif
        return false;
    }

    if (midiBytesToLong(midiHeader.substr(4,4)) != 6L) {
#ifdef MIDI_DEBUG
        SVDEBUG << "MIDIFileReader::parseHeader()"
             << " - header length incorrect"
             << endl;
#endif
        return false;
    }

    m_format = (MIDIFileFormatType) midiBytesToInt(midiHeader.substr(8,2));
    m_numberOfTracks = midiBytesToInt(midiHeader.substr(10,2));
    m_timingDivision = midiBytesToInt(midiHeader.substr(12,2));

    if (m_timingDivision >= 32768) {
        m_smpte = true;
        m_fps = 256 - (m_timingDivision >> 8);
        m_subframes = (m_timingDivision & 0xff);
    } else {
        m_smpte = false;
    }

    return true; 
}

// Extract the contents from a MIDI file track and places it into
// our local map of MIDI events.
//
bool
MIDIFileReader::parseTrack(unsigned int &lastTrackNum)
{
    MIDIByte midiByte, metaEventCode, data1, data2;
    MIDIByte eventCode = 0x80;
    string metaMessage;
    long messageLength;
    long deltaTime;
    long accumulatedTime = 0;

    // The trackNum passed in to this method is the default track for
    // all events provided they're all on the same channel.  If we find
    // events on more than one channel, we increment trackNum and record
    // the mapping from channel to trackNum in this channelTrackMap.
    // We then return the new trackNum by reference so the calling
    // method knows we've got more tracks than expected.

    // This would be a vector<unsigned int> but we need -1 to indicate
    // "not yet used"
    vector<int> channelTrackMap(16, -1);

    // This is used to store the last absolute time found on each track,
    // allowing us to modify delta-times correctly when separating events
    // out from one to multiple tracks
    //
    map<int, unsigned long> trackTimeMap;

    // Meta-events don't have a channel, so we place them in a fixed
    // track number instead
    unsigned int metaTrack = lastTrackNum;

    // Remember the last non-meta status byte (-1 if we haven't seen one)
    int runningStatus = -1;

    bool firstTrack = true;

    while (!m_midiFile->eof() && (m_trackByteCount > 0)) {

        if (eventCode < 0x80) {
#ifdef MIDI_DEBUG
            SVDEBUG << "WARNING: Invalid event code " << eventCode
                 << " in MIDI file" << endl;
#endif
            throw MIDIException(tr("Invalid event code %1 found").arg(int(eventCode)));
        }

        deltaTime = getNumberFromMIDIBytes();

#ifdef MIDI_DEBUG
        SVDEBUG << "read delta time " << deltaTime << endl;
#endif

        // Get a single byte
        midiByte = getMIDIByte();

        if (!(midiByte & MIDI_STATUS_BYTE_MASK)) {

            if (runningStatus < 0) {
                throw MIDIException(tr("Running status used for first event in track"));
            }

            eventCode = (MIDIByte)runningStatus;
            data1 = midiByte;

#ifdef MIDI_DEBUG
            SVDEBUG << "using running status (byte " << int(midiByte) << " found)" << endl;
#endif
        } else {
#ifdef MIDI_DEBUG
            SVDEBUG << "have new event code " << int(midiByte) << endl;
#endif
            eventCode = midiByte;
            data1 = getMIDIByte();
        }

        if (eventCode == MIDI_FILE_META_EVENT) {

            metaEventCode = data1;
            messageLength = getNumberFromMIDIBytes();

//#ifdef MIDI_DEBUG
                SVDEBUG << "Meta event of type " << int(metaEventCode) << " and " << messageLength << " bytes found, putting on track " << metaTrack << endl;
//#endif
            metaMessage = getMIDIBytes(messageLength);

            long gap = accumulatedTime - trackTimeMap[metaTrack];
            accumulatedTime += deltaTime;
            deltaTime += gap;
            trackTimeMap[metaTrack] = accumulatedTime;

            MIDIEvent *e = new MIDIEvent(deltaTime,
                                         MIDI_FILE_META_EVENT,
                                         metaEventCode,
                                         metaMessage);

            m_midiComposition[metaTrack].push_back(e);

            if (metaEventCode == MIDI_TRACK_NAME) {
                m_trackNames[metaTrack] = metaMessage.c_str();
            }

        } else { // non-meta events

            runningStatus = eventCode;

            MIDIEvent *midiEvent;

            int channel = (eventCode & MIDI_CHANNEL_NUM_MASK);
            if (channelTrackMap[channel] == -1) {
                if (!firstTrack) ++lastTrackNum;
                else firstTrack = false;
                channelTrackMap[channel] = lastTrackNum;
            }

            unsigned int trackNum = channelTrackMap[channel];
            
            // accumulatedTime is abs time of last event on any track;
            // trackTimeMap[trackNum] is that of last event on this track
            
            long gap = accumulatedTime - trackTimeMap[trackNum];
            accumulatedTime += deltaTime;
            deltaTime += gap;
            trackTimeMap[trackNum] = accumulatedTime;

            switch (eventCode & MIDI_MESSAGE_TYPE_MASK) {

            case MIDI_NOTE_ON:
            case MIDI_NOTE_OFF:
            case MIDI_POLY_AFTERTOUCH:
            case MIDI_CTRL_CHANGE:
                data2 = getMIDIByte();

                // create and store our event
                midiEvent = new MIDIEvent(deltaTime, eventCode, data1, data2);

                /*
                SVDEBUG << "MIDI event for channel " << channel << " (track "
                          << trackNum << ")" << endl;
                midiEvent->print();
                          */


                m_midiComposition[trackNum].push_back(midiEvent);

                if (midiEvent->getChannelNumber() == MIDI_PERCUSSION_CHANNEL) {
                    m_percussionTracks.insert(trackNum);
                }

                break;

            case MIDI_PITCH_BEND:
                data2 = getMIDIByte();

                // create and store our event
                midiEvent = new MIDIEvent(deltaTime, eventCode, data1, data2);
                m_midiComposition[trackNum].push_back(midiEvent);
                break;

            case MIDI_PROG_CHANGE:
            case MIDI_CHNL_AFTERTOUCH:
                // create and store our event
                midiEvent = new MIDIEvent(deltaTime, eventCode, data1);
                m_midiComposition[trackNum].push_back(midiEvent);
                break;

            case MIDI_SYSTEM_EXCLUSIVE:
                messageLength = getNumberFromMIDIBytes(data1);

#ifdef MIDI_DEBUG
                SVDEBUG << "SysEx of " << messageLength << " bytes found" << endl;
#endif

                metaMessage= getMIDIBytes(messageLength);

                if (MIDIByte(metaMessage[metaMessage.length() - 1]) !=
                        MIDI_END_OF_EXCLUSIVE)
                {
#ifdef MIDI_DEBUG
                    SVDEBUG << "MIDIFileReader::parseTrack() - "
                              << "malformed or unsupported SysEx type"
                              << endl;
#endif
                    continue;
                }

                // chop off the EOX 
                // length fixed by Pedro Lopez-Cabanillas (20030523)
                //
                metaMessage = metaMessage.substr(0, metaMessage.length()-1);

                midiEvent = new MIDIEvent(deltaTime,
                                          MIDI_SYSTEM_EXCLUSIVE,
                                          metaMessage);
                m_midiComposition[trackNum].push_back(midiEvent);
                break;

            default:
#ifdef MIDI_DEBUG
                SVDEBUG << "MIDIFileReader::parseTrack()" 
                          << " - Unsupported MIDI Event Code:  "
                          << (int)eventCode << endl;
#endif
                break;
            } 
        }
    }

    if (lastTrackNum > metaTrack) {
        for (unsigned int track = metaTrack + 1; track <= lastTrackNum; ++track) {
            m_trackNames[track] = QString("%1 <%2>")
                .arg(m_trackNames[metaTrack]).arg(track - metaTrack + 1);
        }
    }

    return true;
}

// Delete dead NOTE OFF and NOTE ON/Zero Velocity Events after
// reading them and modifying their relevant NOTE ONs.  Return true
// if there are some notes in this track.
//
bool
MIDIFileReader::consolidateNoteOffEvents(unsigned int track)
{
    bool notesOnTrack = false;
    bool noteOffFound;

    for (MIDITrack::iterator i = m_midiComposition[track].begin();
         i != m_midiComposition[track].end(); i++) {

        if ((*i)->getMessageType() == MIDI_NOTE_ON && (*i)->getVelocity() > 0) {

            notesOnTrack = true;
            noteOffFound = false;

            for (MIDITrack::iterator j = i;
                 j != m_midiComposition[track].end(); j++) {

                if (((*j)->getChannelNumber() == (*i)->getChannelNumber()) &&
                    ((*j)->getPitch() == (*i)->getPitch()) &&
                    ((*j)->getMessageType() == MIDI_NOTE_OFF ||
                    ((*j)->getMessageType() == MIDI_NOTE_ON &&
                     (*j)->getVelocity() == 0x00))) {

                    (*i)->setDuration((*j)->getTime() - (*i)->getTime());

                    delete *j;
                    m_midiComposition[track].erase(j);

                    noteOffFound = true;
                    break;
                }
            }

            // If no matching NOTE OFF has been found then set
            // Event duration to length of track
            //
            if (!noteOffFound) {
                MIDITrack::iterator j = m_midiComposition[track].end();
                --j;
                (*i)->setDuration((*j)->getTime() - (*i)->getTime());
            }
        }
    }

    return notesOnTrack;
}

// Add any tempo events found in the given track to the global tempo map.
//
void
MIDIFileReader::updateTempoMap(unsigned int track)
{
    SVDEBUG << "updateTempoMap for track " << track << " (" << m_midiComposition[track].size() << " events)" << endl;

    for (MIDITrack::iterator i = m_midiComposition[track].begin();
         i != m_midiComposition[track].end(); ++i) {

        if ((*i)->isMeta() &&
            (*i)->getMetaEventCode() == MIDI_SET_TEMPO) {

            MIDIByte m0 = (*i)->getMetaMessage()[0];
            MIDIByte m1 = (*i)->getMetaMessage()[1];
            MIDIByte m2 = (*i)->getMetaMessage()[2];
            
            long tempo = (((m0 << 8) + m1) << 8) + m2;

            SVDEBUG << "updateTempoMap: have tempo, it's " << tempo << " at " << (*i)->getTime() << endl;

            if (tempo != 0) {
                double qpm = 60000000.0 / double(tempo);
                m_tempoMap[(*i)->getTime()] =
                    TempoChange(RealTime::zeroTime, qpm);
            }
        }
    }
}

void
MIDIFileReader::calculateTempoTimestamps()
{
    unsigned long lastMIDITime = 0;
    RealTime lastRealTime = RealTime::zeroTime;
    double tempo = 120.0;
    int td = m_timingDivision;
    if (td == 0) td = 96;

    for (TempoMap::iterator i = m_tempoMap.begin(); i != m_tempoMap.end(); ++i) {
        
        unsigned long mtime = i->first;
        unsigned long melapsed = mtime - lastMIDITime;
        double quarters = double(melapsed) / double(td);
        double seconds = (60.0 * quarters) / tempo;

        RealTime t = lastRealTime + RealTime::fromSeconds(seconds);

        i->second.first = t;

        lastRealTime = t;
        lastMIDITime = mtime;
        tempo = i->second.second;
    }
}

RealTime
MIDIFileReader::getTimeForMIDITime(unsigned long midiTime) const
{
    unsigned long tempoMIDITime = 0;
    RealTime tempoRealTime = RealTime::zeroTime;
    double tempo = 120.0;

    TempoMap::const_iterator i = m_tempoMap.lower_bound(midiTime);
    if (i != m_tempoMap.begin()) {
        --i;
        tempoMIDITime = i->first;
        tempoRealTime = i->second.first;
        tempo = i->second.second;
    }

    int td = m_timingDivision;
    if (td == 0) td = 96;

    unsigned long melapsed = midiTime - tempoMIDITime;
    double quarters = double(melapsed) / double(td);
    double seconds = (60.0 * quarters) / tempo;

/*
    SVDEBUG << "MIDIFileReader::getTimeForMIDITime(" << midiTime << ")"
              << endl;
    SVDEBUG << "timing division = " << td << endl;
    SVDEBUG << "nearest tempo event (of " << m_tempoMap.size() << ") is at " << tempoMIDITime << " ("
              << tempoRealTime << ")" << endl;
    SVDEBUG << "quarters since then = " << quarters << endl;
    SVDEBUG << "tempo = " << tempo << " quarters per minute" << endl;
    SVDEBUG << "seconds since then = " << seconds << endl;
    SVDEBUG << "resulting time = " << (tempoRealTime + RealTime::fromSeconds(seconds)) << endl;
*/

    return tempoRealTime + RealTime::fromSeconds(seconds);
}

Model *
MIDIFileReader::load() const
{
    if (!isOK()) return nullptr;

    if (m_loadableTracks.empty()) {
        if (m_acquirer) {
            m_acquirer->showError
                (tr("MIDI file \"%1\" has no notes in any track").arg(m_path));
        }
        return nullptr;
    }

    std::set<unsigned int> tracksToLoad;

    if (m_loadableTracks.size() == 1) {

        tracksToLoad.insert(*m_loadableTracks.begin());

    } else {

        QStringList displayNames;

        for (set<unsigned int>::iterator i = m_loadableTracks.begin();
             i != m_loadableTracks.end(); ++i) {

            unsigned int trackNo = *i;
            QString label;

            QString perc;
            if (m_percussionTracks.find(trackNo) != m_percussionTracks.end()) {
                perc = tr(" - uses GM percussion channel");
            }

            if (m_trackNames.find(trackNo) != m_trackNames.end()) {
                label = tr("Track %1 (%2)%3")
                    .arg(trackNo).arg(m_trackNames.find(trackNo)->second)
                    .arg(perc);
            } else {
                label = tr("Track %1 (untitled)%3").arg(trackNo).arg(perc);
            }

            displayNames << label;
        }

        QString singleTrack;

        bool haveSomePercussion = 
            (!m_percussionTracks.empty() &&
             (m_percussionTracks.size() < m_loadableTracks.size()));

        MIDIFileImportPreferenceAcquirer::TrackPreference pref;

        if (m_acquirer) {
            pref = m_acquirer->getTrackImportPreference(displayNames,
                                                        haveSomePercussion,
                                                        singleTrack);
        } else {
            pref = MIDIFileImportPreferenceAcquirer::MergeAllTracks;
        }

        if (pref == MIDIFileImportPreferenceAcquirer::ImportNothing) return nullptr;

        if (pref == MIDIFileImportPreferenceAcquirer::MergeAllTracks ||
            pref == MIDIFileImportPreferenceAcquirer::MergeAllNonPercussionTracks) {
            
            for (set<unsigned int>::iterator i = m_loadableTracks.begin();
                 i != m_loadableTracks.end(); ++i) {
                
                if (pref == MIDIFileImportPreferenceAcquirer::MergeAllTracks ||
                    m_percussionTracks.find(*i) == m_percussionTracks.end()) {
                    
                    tracksToLoad.insert(*i);
                }
            }

        } else {
            
            int j = 0;

            for (set<unsigned int>::iterator i = m_loadableTracks.begin();
                 i != m_loadableTracks.end(); ++i) {
                
                if (singleTrack == displayNames[j]) {
                    tracksToLoad.insert(*i);
                    break;
                }
                
                ++j;
            }
        }
    }

    if (tracksToLoad.empty()) return nullptr;

    int n = int(tracksToLoad.size()), count = 0;
    Model *model = nullptr;

    for (std::set<unsigned int>::iterator i = tracksToLoad.begin();
         i != tracksToLoad.end(); ++i) {

        int minProgress = (100 * count) / n;
        int progressAmount = 100 / n;

        model = loadTrack(*i, model, minProgress, progressAmount);

        ++count;
    }

    if (dynamic_cast<NoteModel *>(model)) {
        dynamic_cast<NoteModel *>(model)->setCompletion(100);
    }

    return model;
}

Model *
MIDIFileReader::loadTrack(unsigned int trackToLoad,
                          Model *existingModel,
                          int minProgress,
                          int progressAmount) const
{
    if (m_midiComposition.find(trackToLoad) == m_midiComposition.end()) {
        return nullptr;
    }

    NoteModel *model = nullptr;

    if (existingModel) {
        model = dynamic_cast<NoteModel *>(existingModel);
        if (!model) {
            SVDEBUG << "WARNING: MIDIFileReader::loadTrack: Existing model given, but it isn't a NoteModel -- ignoring it" << endl;
        }
    }

    if (!model) {
        model = new NoteModel(m_mainModelSampleRate, 1, 0.0, 0.0, false);
        model->setValueQuantization(1.0);
        model->setObjectName(QFileInfo(m_path).fileName());
    }

    const MIDITrack &track = m_midiComposition.find(trackToLoad)->second;

    int totalEvents = int(track.size());
    int count = 0;

    bool sharpKey = true;

    for (MIDITrack::const_iterator i = track.begin(); i != track.end(); ++i) {

        RealTime rt;
        unsigned long midiTime = (*i)->getTime();

        if (m_smpte) {
            rt = RealTime::frame2RealTime(midiTime, m_fps * m_subframes);
        } else {
            rt = getTimeForMIDITime(midiTime);
        }

        // We ignore most of these event types for now, though in
        // theory some of the text ones could usefully be incorporated

        if ((*i)->isMeta()) {

            switch((*i)->getMetaEventCode()) {

            case MIDI_KEY_SIGNATURE:
                // minorKey = (int((*i)->getMetaMessage()[1]) != 0);
                sharpKey = (int((*i)->getMetaMessage()[0]) >= 0);
                break;

            case MIDI_TEXT_EVENT:
            case MIDI_LYRIC:
            case MIDI_TEXT_MARKER:
            case MIDI_COPYRIGHT_NOTICE:
            case MIDI_TRACK_NAME:
                // The text events that we could potentially use
                break;

            case MIDI_SET_TEMPO:
                // Already dealt with in a separate pass previously
                break;

            case MIDI_TIME_SIGNATURE:
                // Not yet!
                break;

            case MIDI_SEQUENCE_NUMBER:
            case MIDI_CHANNEL_PREFIX_OR_PORT:
            case MIDI_INSTRUMENT_NAME:
            case MIDI_CUE_POINT:
            case MIDI_CHANNEL_PREFIX:
            case MIDI_SEQUENCER_SPECIFIC:
            case MIDI_SMPTE_OFFSET:
            default:
                break;
            }

        } else {

            switch ((*i)->getMessageType()) {

            case MIDI_NOTE_ON:

                if ((*i)->getVelocity() == 0) break; // effective note-off
                else {
                    RealTime endRT;
                    unsigned long endMidiTime = (*i)->getTime() + (*i)->getDuration();
                    if (m_smpte) {
                        endRT = RealTime::frame2RealTime(endMidiTime, m_fps * m_subframes);
                    } else {
                        endRT = getTimeForMIDITime(endMidiTime);
                    }

                    long startFrame = RealTime::realTime2Frame
                        (rt, model->getSampleRate());

                    long endFrame = RealTime::realTime2Frame
                        (endRT, model->getSampleRate());

                    QString pitchLabel = Pitch::getPitchLabel((*i)->getPitch(),
                                                              0, 
                                                              !sharpKey);

                    QString noteLabel = tr("%1 - vel %2")
                        .arg(pitchLabel).arg(int((*i)->getVelocity()));

                    float level = float((*i)->getVelocity()) / 128.f;

                    Note note(startFrame, (*i)->getPitch(),
                              endFrame - startFrame, level, noteLabel);

//                    SVDEBUG << "Adding note " << startFrame << "," << (endFrame-startFrame) << " : " << int((*i)->getPitch()) << endl;

                    model->addPoint(note);
                    break;
                }

            case MIDI_PITCH_BEND:
                // I guess we could make some use of this...
                break;

            case MIDI_NOTE_OFF:
            case MIDI_PROG_CHANGE:
            case MIDI_CTRL_CHANGE:
            case MIDI_SYSTEM_EXCLUSIVE:
            case MIDI_POLY_AFTERTOUCH:
            case MIDI_CHNL_AFTERTOUCH:
                break;

            default:
                break;
            }
        }

        model->setCompletion(minProgress +
                             (count * progressAmount) / totalEvents);
        ++count;
    }

    return model;
}