Mercurial > hg > svcore
diff data/fileio/MIDIFileReader.cpp @ 148:1a42221a1522
* Reorganising code base. This revision will not compile.
author | Chris Cannam |
---|---|
date | Mon, 31 Jul 2006 11:49:58 +0000 |
parents | |
children | 4b2ea82fd0ed |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/fileio/MIDIFileReader.cpp Mon Jul 31 11:49:58 2006 +0000 @@ -0,0 +1,1248 @@ +/* -*- 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 "base/Model.h" +#include "base/Pitch.h" +#include "base/RealTime.h" +#include "model/NoteModel.h" + +#include <QString> +#include <QMessageBox> +#include <QInputDialog> + +#include <sstream> + +using std::string; +using std::ifstream; +using std::stringstream; +using std::cerr; +using std::endl; +using std::ends; +using std::ios; +using std::vector; +using std::map; +using std::set; + +//#define MIDI_DEBUG 1 + +static const char *const MIDI_FILE_HEADER = "MThd"; +static const char *const MIDI_TRACK_HEADER = "MTrk"; + +static const MIDIFileReader::MIDIByte MIDI_STATUS_BYTE_MASK = 0x80; +static const MIDIFileReader::MIDIByte MIDI_MESSAGE_TYPE_MASK = 0xF0; +static const MIDIFileReader::MIDIByte MIDI_CHANNEL_NUM_MASK = 0x0F; +static const MIDIFileReader::MIDIByte MIDI_NOTE_OFF = 0x80; +static const MIDIFileReader::MIDIByte MIDI_NOTE_ON = 0x90; +static const MIDIFileReader::MIDIByte MIDI_POLY_AFTERTOUCH = 0xA0; +static const MIDIFileReader::MIDIByte MIDI_CTRL_CHANGE = 0xB0; +static const MIDIFileReader::MIDIByte MIDI_PROG_CHANGE = 0xC0; +static const MIDIFileReader::MIDIByte MIDI_CHNL_AFTERTOUCH = 0xD0; +static const MIDIFileReader::MIDIByte MIDI_PITCH_BEND = 0xE0; +static const MIDIFileReader::MIDIByte MIDI_SELECT_CHNL_MODE = 0xB0; +static const MIDIFileReader::MIDIByte MIDI_SYSTEM_EXCLUSIVE = 0xF0; +static const MIDIFileReader::MIDIByte MIDI_TC_QUARTER_FRAME = 0xF1; +static const MIDIFileReader::MIDIByte MIDI_SONG_POSITION_PTR = 0xF2; +static const MIDIFileReader::MIDIByte MIDI_SONG_SELECT = 0xF3; +static const MIDIFileReader::MIDIByte MIDI_TUNE_REQUEST = 0xF6; +static const MIDIFileReader::MIDIByte MIDI_END_OF_EXCLUSIVE = 0xF7; +static const MIDIFileReader::MIDIByte MIDI_TIMING_CLOCK = 0xF8; +static const MIDIFileReader::MIDIByte MIDI_START = 0xFA; +static const MIDIFileReader::MIDIByte MIDI_CONTINUE = 0xFB; +static const MIDIFileReader::MIDIByte MIDI_STOP = 0xFC; +static const MIDIFileReader::MIDIByte MIDI_ACTIVE_SENSING = 0xFE; +static const MIDIFileReader::MIDIByte MIDI_SYSTEM_RESET = 0xFF; +static const MIDIFileReader::MIDIByte MIDI_SYSEX_NONCOMMERCIAL = 0x7D; +static const MIDIFileReader::MIDIByte MIDI_SYSEX_NON_RT = 0x7E; +static const MIDIFileReader::MIDIByte MIDI_SYSEX_RT = 0x7F; +static const MIDIFileReader::MIDIByte MIDI_SYSEX_RT_COMMAND = 0x06; +static const MIDIFileReader::MIDIByte MIDI_SYSEX_RT_RESPONSE = 0x07; +static const MIDIFileReader::MIDIByte MIDI_MMC_STOP = 0x01; +static const MIDIFileReader::MIDIByte MIDI_MMC_PLAY = 0x02; +static const MIDIFileReader::MIDIByte MIDI_MMC_DEFERRED_PLAY = 0x03; +static const MIDIFileReader::MIDIByte MIDI_MMC_FAST_FORWARD = 0x04; +static const MIDIFileReader::MIDIByte MIDI_MMC_REWIND = 0x05; +static const MIDIFileReader::MIDIByte MIDI_MMC_RECORD_STROBE = 0x06; +static const MIDIFileReader::MIDIByte MIDI_MMC_RECORD_EXIT = 0x07; +static const MIDIFileReader::MIDIByte MIDI_MMC_RECORD_PAUSE = 0x08; +static const MIDIFileReader::MIDIByte MIDI_MMC_PAUSE = 0x08; +static const MIDIFileReader::MIDIByte MIDI_MMC_EJECT = 0x0A; +static const MIDIFileReader::MIDIByte MIDI_MMC_LOCATE = 0x44; +static const MIDIFileReader::MIDIByte MIDI_FILE_META_EVENT = 0xFF; +static const MIDIFileReader::MIDIByte MIDI_SEQUENCE_NUMBER = 0x00; +static const MIDIFileReader::MIDIByte MIDI_TEXT_EVENT = 0x01; +static const MIDIFileReader::MIDIByte MIDI_COPYRIGHT_NOTICE = 0x02; +static const MIDIFileReader::MIDIByte MIDI_TRACK_NAME = 0x03; +static const MIDIFileReader::MIDIByte MIDI_INSTRUMENT_NAME = 0x04; +static const MIDIFileReader::MIDIByte MIDI_LYRIC = 0x05; +static const MIDIFileReader::MIDIByte MIDI_TEXT_MARKER = 0x06; +static const MIDIFileReader::MIDIByte MIDI_CUE_POINT = 0x07; +static const MIDIFileReader::MIDIByte MIDI_CHANNEL_PREFIX = 0x20; +static const MIDIFileReader::MIDIByte MIDI_CHANNEL_PREFIX_OR_PORT = 0x21; +static const MIDIFileReader::MIDIByte MIDI_END_OF_TRACK = 0x2F; +static const MIDIFileReader::MIDIByte MIDI_SET_TEMPO = 0x51; +static const MIDIFileReader::MIDIByte MIDI_SMPTE_OFFSET = 0x54; +static const MIDIFileReader::MIDIByte MIDI_TIME_SIGNATURE = 0x58; +static const MIDIFileReader::MIDIByte MIDI_KEY_SIGNATURE = 0x59; +static const MIDIFileReader::MIDIByte MIDI_SEQUENCER_SPECIFIC = 0x7F; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_BANK_MSB = 0x00; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_VOLUME = 0x07; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_BANK_LSB = 0x20; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_MODULATION = 0x01; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_PAN = 0x0A; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_SUSTAIN = 0x40; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_RESONANCE = 0x47; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_RELEASE = 0x48; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_ATTACK = 0x49; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_FILTER = 0x4A; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_REVERB = 0x5B; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_CHORUS = 0x5D; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_NRPN_1 = 0x62; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_NRPN_2 = 0x63; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_RPN_1 = 0x64; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_RPN_2 = 0x65; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_SOUNDS_OFF = 0x78; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_RESET = 0x79; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_LOCAL = 0x7A; +static const MIDIFileReader::MIDIByte MIDI_CONTROLLER_ALL_NOTES_OFF = 0x7B; +static const MIDIFileReader::MIDIByte MIDI_PERCUSSION_CHANNEL = 9; + +class MIDIEvent +{ +public: + typedef MIDIFileReader::MIDIByte MIDIByte; + + MIDIEvent(unsigned long deltaTime, + MIDIByte eventCode, + MIDIByte data1 = 0, + MIDIByte data2 = 0) : + m_deltaTime(deltaTime), + m_duration(0), + m_eventCode(eventCode), + m_data1(data1), + m_data2(data2), + m_metaEventCode(0) + { } + + MIDIEvent(unsigned long deltaTime, + MIDIByte eventCode, + MIDIByte metaEventCode, + const string &metaMessage) : + m_deltaTime(deltaTime), + m_duration(0), + m_eventCode(eventCode), + m_data1(0), + m_data2(0), + m_metaEventCode(metaEventCode), + m_metaMessage(metaMessage) + { } + + MIDIEvent(unsigned long deltaTime, + MIDIByte eventCode, + const string &sysEx) : + m_deltaTime(deltaTime), + m_duration(0), + m_eventCode(eventCode), + m_data1(0), + m_data2(0), + m_metaEventCode(0), + m_metaMessage(sysEx) + { } + + ~MIDIEvent() { } + + void setTime(const unsigned long &time) { m_deltaTime = time; } + void setDuration(const unsigned long& duration) { m_duration = duration;} + unsigned long addTime(const unsigned long &time) { + m_deltaTime += time; + return m_deltaTime; + } + + MIDIByte getMessageType() const + { return (m_eventCode & MIDI_MESSAGE_TYPE_MASK); } + + MIDIByte getChannelNumber() const + { return (m_eventCode & MIDI_CHANNEL_NUM_MASK); } + + unsigned long getTime() const { return m_deltaTime; } + unsigned long getDuration() const { return m_duration; } + + MIDIByte getPitch() const { return m_data1; } + MIDIByte getVelocity() const { return m_data2; } + MIDIByte getData1() const { return m_data1; } + MIDIByte getData2() const { return m_data2; } + MIDIByte getEventCode() const { return m_eventCode; } + + bool isMeta() const { return (m_eventCode == MIDI_FILE_META_EVENT); } + + MIDIByte getMetaEventCode() const { return m_metaEventCode; } + string getMetaMessage() const { return m_metaMessage; } + void setMetaMessage(const string &meta) { m_metaMessage = meta; } + + friend bool operator<(const MIDIEvent &a, const MIDIEvent &b); + +private: + MIDIEvent& operator=(const MIDIEvent); + + unsigned long m_deltaTime; + unsigned long m_duration; + MIDIByte m_eventCode; + MIDIByte m_data1; // or Note + MIDIByte m_data2; // or Velocity + MIDIByte m_metaEventCode; + string m_metaMessage; +}; + +// Comparator for sorting +// +struct MIDIEventCmp +{ + bool operator()(const MIDIEvent &mE1, const MIDIEvent &mE2) const + { return mE1.getTime() < mE2.getTime(); } + + bool operator()(const MIDIEvent *mE1, const MIDIEvent *mE2) const + { return mE1->getTime() < mE2->getTime(); } +}; + +class MIDIException : virtual public std::exception +{ +public: + MIDIException(QString message) throw() : m_message(message) { + cerr << "WARNING: MIDI exception: " + << message.toLocal8Bit().data() << endl; + } + virtual ~MIDIException() throw() { } + + virtual const char *what() const throw() { + return m_message.toLocal8Bit().data(); + } + +protected: + QString m_message; +}; + + +MIDIFileReader::MIDIFileReader(QString path, + size_t mainModelSampleRate) : + m_timingDivision(0), + m_format(MIDI_FILE_BAD_FORMAT), + m_numberOfTracks(0), + m_trackByteCount(0), + m_decrementCount(false), + m_path(path), + m_midiFile(0), + m_fileSize(0), + m_mainModelSampleRate(mainModelSampleRate) +{ + 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. +// +MIDIFileReader::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 + cerr << "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; + return false; + } + + bool retval = false; + + try { + + // Set file size so we can count it off + // + m_midiFile->seekg(0, ios::end); + m_fileSize = m_midiFile->tellg(); + 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 + cerr << "Parsing Track " << j << endl; +#endif + + if (!skipToNextTrack()) { +#ifdef MIDI_DEBUG + cerr << "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 + cerr << "Track has " << m_trackByteCount << " bytes" << endl; +#endif + + // Run through the events taking them into our internal + // representation. + if (!parseTrack(i)) { +#ifdef MIDI_DEBUG + cerr << "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 (MIDIException e) { + + cerr << "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 + cerr << "MIDIFileReader::parseHeader() - file header undersized" << endl; +#endif + return false; + } + + if (midiHeader.compare(0, 4, MIDI_FILE_HEADER) != 0) { +#ifdef MIDI_DEBUG + cerr << "MIDIFileReader::parseHeader()" + << "- file header not found or malformed" + << endl; +#endif + return false; + } + + if (midiBytesToLong(midiHeader.substr(4,4)) != 6L) { +#ifdef MIDI_DEBUG + cerr << "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_format == MIDI_SEQUENTIAL_TRACK_FILE) { +#ifdef MIDI_DEBUG + cerr << "MIDIFileReader::parseHeader()" + << "- can't load sequential track file" + << endl; +#endif + return false; + } + +#ifdef MIDI_DEBUG + if (m_timingDivision < 0) { + cerr << "MIDIFileReader::parseHeader()" + << " - file uses SMPTE timing" + << endl; + } +#endif + + 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; + unsigned int messageLength; + unsigned long deltaTime; + unsigned 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 + cerr << "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 + cerr << "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 + cerr << "using running status (byte " << int(midiByte) << " found)" << endl; +#endif + } else { +#ifdef MIDI_DEBUG + cerr << "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 + cerr << "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); + + /* + cerr << "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 + cerr << "SysEx of " << messageLength << " bytes found" << endl; +#endif + + metaMessage= getMIDIBytes(messageLength); + + if (MIDIByte(metaMessage[metaMessage.length() - 1]) != + MIDI_END_OF_EXCLUSIVE) + { +#ifdef MIDI_DEBUG + cerr << "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; + + case MIDI_END_OF_EXCLUSIVE: +#ifdef MIDI_DEBUG + cerr << "MIDIFileReader::parseTrack() - " + << "Found a stray MIDI_END_OF_EXCLUSIVE" << endl; +#endif + break; + + default: +#ifdef MIDI_DEBUG + cerr << "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) +{ + std::cerr << "updateTempoMap for track " << track << " (" << m_midiComposition[track].size() << " events)" << std::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; + + std::cerr << "updateTempoMap: have tempo, it's " << tempo << " at " << (*i)->getTime() << std::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; + +/* + std::cerr << "MIDIFileReader::getTimeForMIDITime(" << midiTime << ")" + << std::endl; + std::cerr << "timing division = " << td << std::endl; + std::cerr << "nearest tempo event (of " << m_tempoMap.size() << ") is at " << tempoMIDITime << " (" + << tempoRealTime << ")" << std::endl; + std::cerr << "quarters since then = " << quarters << std::endl; + std::cerr << "tempo = " << tempo << " quarters per minute" << std::endl; + std::cerr << "seconds since then = " << seconds << std::endl; + std::cerr << "resulting time = " << (tempoRealTime + RealTime::fromSeconds(seconds)) << std::endl; +*/ + + return tempoRealTime + RealTime::fromSeconds(seconds); +} + +Model * +MIDIFileReader::load() const +{ + if (!isOK()) return 0; + + if (m_loadableTracks.empty()) { + QMessageBox::critical(0, tr("No notes in MIDI file"), + tr("MIDI file \"%1\" has no notes in any track") + .arg(m_path)); + return 0; + } + + std::set<unsigned int> tracksToLoad; + + if (m_loadableTracks.size() == 1) { + + tracksToLoad.insert(*m_loadableTracks.begin()); + + } else { + + QStringList available; + QString allTracks = tr("Merge all tracks"); + QString allNonPercussion = tr("Merge all non-percussion tracks"); + + int nonTrackItems = 1; + + available << allTracks; + + if (!m_percussionTracks.empty() && + (m_percussionTracks.size() < m_loadableTracks.size())) { + available << allNonPercussion; + ++nonTrackItems; + } + + 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); + } + available << label; + } + + bool ok = false; + QString selected = QInputDialog::getItem + (0, tr("Select track or tracks to import"), + tr("You can only import this file as a single annotation layer,\nbut the file contains more than one track,\nor notes on more than one channel.\n\nPlease select the track or merged tracks you wish to import:"), + available, 0, false, &ok); + + if (!ok || selected.isEmpty()) return 0; + + if (selected == allTracks || selected == allNonPercussion) { + + for (set<unsigned int>::iterator i = m_loadableTracks.begin(); + i != m_loadableTracks.end(); ++i) { + + if (selected == allTracks || + m_percussionTracks.find(*i) == m_percussionTracks.end()) { + + tracksToLoad.insert(*i); + } + } + + } else { + + int j = nonTrackItems; + + for (set<unsigned int>::iterator i = m_loadableTracks.begin(); + i != m_loadableTracks.end(); ++i) { + + if (selected == available[j]) { + tracksToLoad.insert(*i); + break; + } + + ++j; + } + } + } + + if (tracksToLoad.empty()) return 0; + + size_t n = tracksToLoad.size(), count = 0; + Model *model = 0; + + 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 0; + } + + NoteModel *model = 0; + + if (existingModel) { + model = dynamic_cast<NoteModel *>(existingModel); + if (!model) { + std::cerr << "WARNING: MIDIFileReader::loadTrack: Existing model given, but it isn't a NoteModel -- ignoring it" << std::endl; + } + } + + if (!model) { + model = new NoteModel(m_mainModelSampleRate, 1, 0.0, 0.0, false); + model->setValueQuantization(1.0); + } + + const MIDITrack &track = m_midiComposition.find(trackToLoad)->second; + + size_t totalEvents = track.size(); + size_t count = 0; + + bool minorKey = false; + bool sharpKey = true; + + for (MIDITrack::const_iterator i = track.begin(); i != track.end(); ++i) { + + RealTime rt = getTimeForMIDITime((*i)->getTime()); + + // 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 = getTimeForMIDITime((*i)->getTime() + + (*i)->getDuration()); + + 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())); + + Note note(startFrame, (*i)->getPitch(), + endFrame - startFrame, noteLabel); + +// std::cerr << "Adding note " << startFrame << "," << (endFrame-startFrame) << " : " << int((*i)->getPitch()) << std::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; +} + +