annotate data/fileio/MIDIFileReader.cpp @ 1008:d9e0e59a1581

When using an aggregate model to pass data to a transform, zero-pad the shorter input to the duration of the longer rather than truncating the longer. (This is better behaviour for e.g. MATCH, and in any case the code was previously truncating incorrectly and ending up with garbage data at the end.)
author Chris Cannam
date Fri, 14 Nov 2014 13:51:33 +0000
parents 2d5a8219b4b0
children 920699b6989d
rev   line source
Chris@148 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@148 2
Chris@148 3 /*
Chris@148 4 Sonic Visualiser
Chris@148 5 An audio file viewer and annotation editor.
Chris@148 6 Centre for Digital Music, Queen Mary, University of London.
Chris@148 7
Chris@148 8 This program is free software; you can redistribute it and/or
Chris@148 9 modify it under the terms of the GNU General Public License as
Chris@148 10 published by the Free Software Foundation; either version 2 of the
Chris@148 11 License, or (at your option) any later version. See the file
Chris@148 12 COPYING included with this distribution for more information.
Chris@148 13 */
Chris@148 14
Chris@148 15
Chris@148 16 /*
Chris@148 17 This is a modified version of a source file from the
Chris@148 18 Rosegarden MIDI and audio sequencer and notation editor.
Chris@148 19 This file copyright 2000-2006 Richard Bown and Chris Cannam.
Chris@148 20 */
Chris@148 21
Chris@148 22
Chris@148 23 #include <iostream>
Chris@148 24 #include <fstream>
Chris@148 25 #include <string>
Chris@148 26 #include <cstdio>
Chris@148 27 #include <algorithm>
Chris@148 28
Chris@148 29 #include "MIDIFileReader.h"
Chris@148 30
Chris@560 31 #include "data/midi/MIDIEvent.h"
Chris@301 32
Chris@150 33 #include "model/Model.h"
Chris@148 34 #include "base/Pitch.h"
Chris@148 35 #include "base/RealTime.h"
Chris@148 36 #include "model/NoteModel.h"
Chris@148 37
Chris@148 38 #include <QString>
Chris@148 39
Chris@148 40 #include <sstream>
Chris@148 41
Chris@843 42 #include "base/Debug.h"
Chris@843 43
Chris@148 44 using std::string;
Chris@148 45 using std::ifstream;
Chris@148 46 using std::stringstream;
Chris@148 47 using std::ends;
Chris@148 48 using std::ios;
Chris@148 49 using std::vector;
Chris@148 50 using std::map;
Chris@148 51 using std::set;
Chris@148 52
Chris@301 53 using namespace MIDIConstants;
Chris@301 54
Chris@690 55 //#define MIDI_SVDEBUG 1
Chris@148 56
Chris@148 57
Chris@148 58 MIDIFileReader::MIDIFileReader(QString path,
Chris@392 59 MIDIFileImportPreferenceAcquirer *acquirer,
Chris@929 60 int mainModelSampleRate) :
Chris@613 61 m_smpte(false),
Chris@148 62 m_timingDivision(0),
Chris@613 63 m_fps(0),
Chris@613 64 m_subframes(0),
Chris@148 65 m_format(MIDI_FILE_BAD_FORMAT),
Chris@148 66 m_numberOfTracks(0),
Chris@148 67 m_trackByteCount(0),
Chris@148 68 m_decrementCount(false),
Chris@148 69 m_path(path),
Chris@148 70 m_midiFile(0),
Chris@148 71 m_fileSize(0),
Chris@392 72 m_mainModelSampleRate(mainModelSampleRate),
Chris@392 73 m_acquirer(acquirer)
Chris@148 74 {
Chris@148 75 if (parseFile()) {
Chris@148 76 m_error = "";
Chris@148 77 }
Chris@148 78 }
Chris@148 79
Chris@148 80 MIDIFileReader::~MIDIFileReader()
Chris@148 81 {
Chris@148 82 for (MIDIComposition::iterator i = m_midiComposition.begin();
Chris@148 83 i != m_midiComposition.end(); ++i) {
Chris@148 84
Chris@148 85 for (MIDITrack::iterator j = i->second.begin();
Chris@148 86 j != i->second.end(); ++j) {
Chris@148 87 delete *j;
Chris@148 88 }
Chris@148 89
Chris@148 90 i->second.clear();
Chris@148 91 }
Chris@148 92
Chris@148 93 m_midiComposition.clear();
Chris@148 94 }
Chris@148 95
Chris@148 96 bool
Chris@148 97 MIDIFileReader::isOK() const
Chris@148 98 {
Chris@148 99 return (m_error == "");
Chris@148 100 }
Chris@148 101
Chris@148 102 QString
Chris@148 103 MIDIFileReader::getError() const
Chris@148 104 {
Chris@148 105 return m_error;
Chris@148 106 }
Chris@148 107
Chris@148 108 long
Chris@148 109 MIDIFileReader::midiBytesToLong(const string& bytes)
Chris@148 110 {
Chris@148 111 if (bytes.length() != 4) {
Chris@148 112 throw MIDIException(tr("Wrong length for long data in MIDI stream (%1, should be %2)").arg(bytes.length()).arg(4));
Chris@148 113 }
Chris@148 114
Chris@148 115 long longRet = ((long)(((MIDIByte)bytes[0]) << 24)) |
Chris@148 116 ((long)(((MIDIByte)bytes[1]) << 16)) |
Chris@148 117 ((long)(((MIDIByte)bytes[2]) << 8)) |
Chris@148 118 ((long)((MIDIByte)(bytes[3])));
Chris@148 119
Chris@148 120 return longRet;
Chris@148 121 }
Chris@148 122
Chris@148 123 int
Chris@148 124 MIDIFileReader::midiBytesToInt(const string& bytes)
Chris@148 125 {
Chris@148 126 if (bytes.length() != 2) {
Chris@148 127 throw MIDIException(tr("Wrong length for int data in MIDI stream (%1, should be %2)").arg(bytes.length()).arg(2));
Chris@148 128 }
Chris@148 129
Chris@148 130 int intRet = ((int)(((MIDIByte)bytes[0]) << 8)) |
Chris@148 131 ((int)(((MIDIByte)bytes[1])));
Chris@148 132 return(intRet);
Chris@148 133 }
Chris@148 134
Chris@148 135
Chris@148 136 // Gets a single byte from the MIDI byte stream. For each track
Chris@148 137 // section we can read only a specified number of bytes held in
Chris@148 138 // m_trackByteCount.
Chris@148 139 //
Chris@301 140 MIDIByte
Chris@148 141 MIDIFileReader::getMIDIByte()
Chris@148 142 {
Chris@148 143 if (!m_midiFile) {
Chris@148 144 throw MIDIException(tr("getMIDIByte called but no MIDI file open"));
Chris@148 145 }
Chris@148 146
Chris@148 147 if (m_midiFile->eof()) {
Chris@148 148 throw MIDIException(tr("End of MIDI file encountered while reading"));
Chris@148 149 }
Chris@148 150
Chris@148 151 if (m_decrementCount && m_trackByteCount <= 0) {
Chris@148 152 throw MIDIException(tr("Attempt to get more bytes than expected on Track"));
Chris@148 153 }
Chris@148 154
Chris@148 155 char byte;
Chris@148 156 if (m_midiFile->read(&byte, 1)) {
Chris@148 157 --m_trackByteCount;
Chris@148 158 return (MIDIByte)byte;
Chris@148 159 }
Chris@148 160
Chris@148 161 throw MIDIException(tr("Attempt to read past MIDI file end"));
Chris@148 162 }
Chris@148 163
Chris@148 164
Chris@148 165 // Gets a specified number of bytes from the MIDI byte stream. For
Chris@148 166 // each track section we can read only a specified number of bytes
Chris@148 167 // held in m_trackByteCount.
Chris@148 168 //
Chris@148 169 string
Chris@148 170 MIDIFileReader::getMIDIBytes(unsigned long numberOfBytes)
Chris@148 171 {
Chris@148 172 if (!m_midiFile) {
Chris@148 173 throw MIDIException(tr("getMIDIBytes called but no MIDI file open"));
Chris@148 174 }
Chris@148 175
Chris@148 176 if (m_midiFile->eof()) {
Chris@148 177 throw MIDIException(tr("End of MIDI file encountered while reading"));
Chris@148 178 }
Chris@148 179
Chris@148 180 if (m_decrementCount && (numberOfBytes > (unsigned long)m_trackByteCount)) {
Chris@148 181 throw MIDIException(tr("Attempt to get more bytes than available on Track (%1, only have %2)").arg(numberOfBytes).arg(m_trackByteCount));
Chris@148 182 }
Chris@148 183
Chris@148 184 string stringRet;
Chris@148 185 char fileMIDIByte;
Chris@148 186
Chris@148 187 while (stringRet.length() < numberOfBytes &&
Chris@148 188 m_midiFile->read(&fileMIDIByte, 1)) {
Chris@148 189 stringRet += fileMIDIByte;
Chris@148 190 }
Chris@148 191
Chris@148 192 // if we've reached the end of file without fulfilling the
Chris@148 193 // quota then panic as our parsing has performed incorrectly
Chris@148 194 //
Chris@148 195 if (stringRet.length() < numberOfBytes) {
Chris@148 196 stringRet = "";
Chris@148 197 throw MIDIException(tr("Attempt to read past MIDI file end"));
Chris@148 198 }
Chris@148 199
Chris@148 200 // decrement the byte count
Chris@148 201 if (m_decrementCount)
Chris@148 202 m_trackByteCount -= stringRet.length();
Chris@148 203
Chris@148 204 return stringRet;
Chris@148 205 }
Chris@148 206
Chris@148 207
Chris@148 208 // Get a long number of variable length from the MIDI byte stream.
Chris@148 209 //
Chris@148 210 long
Chris@148 211 MIDIFileReader::getNumberFromMIDIBytes(int firstByte)
Chris@148 212 {
Chris@148 213 if (!m_midiFile) {
Chris@148 214 throw MIDIException(tr("getNumberFromMIDIBytes called but no MIDI file open"));
Chris@148 215 }
Chris@148 216
Chris@148 217 long longRet = 0;
Chris@148 218 MIDIByte midiByte;
Chris@148 219
Chris@148 220 if (firstByte >= 0) {
Chris@148 221 midiByte = (MIDIByte)firstByte;
Chris@148 222 } else if (m_midiFile->eof()) {
Chris@148 223 return longRet;
Chris@148 224 } else {
Chris@148 225 midiByte = getMIDIByte();
Chris@148 226 }
Chris@148 227
Chris@148 228 longRet = midiByte;
Chris@148 229 if (midiByte & 0x80) {
Chris@148 230 longRet &= 0x7F;
Chris@148 231 do {
Chris@148 232 midiByte = getMIDIByte();
Chris@148 233 longRet = (longRet << 7) + (midiByte & 0x7F);
Chris@148 234 } while (!m_midiFile->eof() && (midiByte & 0x80));
Chris@148 235 }
Chris@148 236
Chris@148 237 return longRet;
Chris@148 238 }
Chris@148 239
Chris@148 240
Chris@148 241 // Seek to the next track in the midi file and set the number
Chris@148 242 // of bytes to be read in the counter m_trackByteCount.
Chris@148 243 //
Chris@148 244 bool
Chris@148 245 MIDIFileReader::skipToNextTrack()
Chris@148 246 {
Chris@148 247 if (!m_midiFile) {
Chris@148 248 throw MIDIException(tr("skipToNextTrack called but no MIDI file open"));
Chris@148 249 }
Chris@148 250
Chris@148 251 string buffer, buffer2;
Chris@148 252 m_trackByteCount = -1;
Chris@148 253 m_decrementCount = false;
Chris@148 254
Chris@148 255 while (!m_midiFile->eof() && (m_decrementCount == false)) {
Chris@148 256 buffer = getMIDIBytes(4);
Chris@148 257 if (buffer.compare(0, 4, MIDI_TRACK_HEADER) == 0) {
Chris@148 258 m_trackByteCount = midiBytesToLong(getMIDIBytes(4));
Chris@148 259 m_decrementCount = true;
Chris@148 260 }
Chris@148 261 }
Chris@148 262
Chris@148 263 if (m_trackByteCount == -1) { // we haven't found a track
Chris@148 264 return false;
Chris@148 265 } else {
Chris@148 266 return true;
Chris@148 267 }
Chris@148 268 }
Chris@148 269
Chris@148 270
Chris@148 271 // Read in a MIDI file. The parsing process throws exceptions back up
Chris@148 272 // here if we run into trouble which we can then pass back out to
Chris@148 273 // whoever called us using a nice bool.
Chris@148 274 //
Chris@148 275 bool
Chris@148 276 MIDIFileReader::parseFile()
Chris@148 277 {
Chris@148 278 m_error = "";
Chris@148 279
Chris@148 280 #ifdef MIDI_DEBUG
Chris@690 281 SVDEBUG << "MIDIFileReader::open() : fileName = " << m_fileName.c_str() << endl;
Chris@148 282 #endif
Chris@148 283
Chris@148 284 // Open the file
Chris@148 285 m_midiFile = new ifstream(m_path.toLocal8Bit().data(),
Chris@148 286 ios::in | ios::binary);
Chris@148 287
Chris@148 288 if (!*m_midiFile) {
Chris@148 289 m_error = "File not found or not readable.";
Chris@148 290 m_format = MIDI_FILE_BAD_FORMAT;
Chris@148 291 delete m_midiFile;
Chris@301 292 m_midiFile = 0;
Chris@148 293 return false;
Chris@148 294 }
Chris@148 295
Chris@148 296 bool retval = false;
Chris@148 297
Chris@148 298 try {
Chris@148 299
Chris@148 300 // Set file size so we can count it off
Chris@148 301 //
Chris@148 302 m_midiFile->seekg(0, ios::end);
Chris@148 303 m_fileSize = m_midiFile->tellg();
Chris@148 304 m_midiFile->seekg(0, ios::beg);
Chris@148 305
Chris@148 306 // Parse the MIDI header first. The first 14 bytes of the file.
Chris@148 307 if (!parseHeader(getMIDIBytes(14))) {
Chris@148 308 m_format = MIDI_FILE_BAD_FORMAT;
Chris@148 309 m_error = "Not a MIDI file.";
Chris@148 310 goto done;
Chris@148 311 }
Chris@148 312
Chris@148 313 unsigned int i = 0;
Chris@148 314
Chris@148 315 for (unsigned int j = 0; j < m_numberOfTracks; ++j) {
Chris@148 316
Chris@148 317 #ifdef MIDI_DEBUG
Chris@690 318 SVDEBUG << "Parsing Track " << j << endl;
Chris@148 319 #endif
Chris@148 320
Chris@148 321 if (!skipToNextTrack()) {
Chris@148 322 #ifdef MIDI_DEBUG
Chris@148 323 cerr << "Couldn't find Track " << j << endl;
Chris@148 324 #endif
Chris@148 325 m_error = "File corrupted or in non-standard format?";
Chris@148 326 m_format = MIDI_FILE_BAD_FORMAT;
Chris@148 327 goto done;
Chris@148 328 }
Chris@148 329
Chris@148 330 #ifdef MIDI_DEBUG
Chris@148 331 cerr << "Track has " << m_trackByteCount << " bytes" << endl;
Chris@148 332 #endif
Chris@148 333
Chris@148 334 // Run through the events taking them into our internal
Chris@148 335 // representation.
Chris@148 336 if (!parseTrack(i)) {
Chris@148 337 #ifdef MIDI_DEBUG
Chris@148 338 cerr << "Track " << j << " parsing failed" << endl;
Chris@148 339 #endif
Chris@148 340 m_error = "File corrupted or in non-standard format?";
Chris@148 341 m_format = MIDI_FILE_BAD_FORMAT;
Chris@148 342 goto done;
Chris@148 343 }
Chris@148 344
Chris@148 345 ++i; // j is the source track number, i the destination
Chris@148 346 }
Chris@148 347
Chris@148 348 m_numberOfTracks = i;
Chris@148 349 retval = true;
Chris@148 350
Chris@148 351 } catch (MIDIException e) {
Chris@148 352
Chris@690 353 SVDEBUG << "MIDIFileReader::open() - caught exception - " << e.what() << endl;
Chris@148 354 m_error = e.what();
Chris@148 355 }
Chris@148 356
Chris@148 357 done:
Chris@148 358 m_midiFile->close();
Chris@148 359 delete m_midiFile;
Chris@148 360
Chris@148 361 for (unsigned int track = 0; track < m_numberOfTracks; ++track) {
Chris@148 362
Chris@148 363 // Convert the deltaTime to an absolute time since the track
Chris@148 364 // start. The addTime method returns the sum of the current
Chris@148 365 // MIDI Event delta time plus the argument.
Chris@148 366
Chris@148 367 unsigned long acc = 0;
Chris@148 368
Chris@148 369 for (MIDITrack::iterator i = m_midiComposition[track].begin();
Chris@148 370 i != m_midiComposition[track].end(); ++i) {
Chris@148 371 acc = (*i)->addTime(acc);
Chris@148 372 }
Chris@148 373
Chris@148 374 if (consolidateNoteOffEvents(track)) { // returns true if some notes exist
Chris@148 375 m_loadableTracks.insert(track);
Chris@148 376 }
Chris@148 377 }
Chris@148 378
Chris@148 379 for (unsigned int track = 0; track < m_numberOfTracks; ++track) {
Chris@148 380 updateTempoMap(track);
Chris@148 381 }
Chris@148 382
Chris@148 383 calculateTempoTimestamps();
Chris@148 384
Chris@148 385 return retval;
Chris@148 386 }
Chris@148 387
Chris@148 388 // Parse and ensure the MIDI Header is legitimate
Chris@148 389 //
Chris@148 390 bool
Chris@148 391 MIDIFileReader::parseHeader(const string &midiHeader)
Chris@148 392 {
Chris@148 393 if (midiHeader.size() < 14) {
Chris@148 394 #ifdef MIDI_DEBUG
Chris@690 395 SVDEBUG << "MIDIFileReader::parseHeader() - file header undersized" << endl;
Chris@148 396 #endif
Chris@148 397 return false;
Chris@148 398 }
Chris@148 399
Chris@148 400 if (midiHeader.compare(0, 4, MIDI_FILE_HEADER) != 0) {
Chris@148 401 #ifdef MIDI_DEBUG
Chris@690 402 SVDEBUG << "MIDIFileReader::parseHeader()"
Chris@148 403 << "- file header not found or malformed"
Chris@148 404 << endl;
Chris@148 405 #endif
Chris@148 406 return false;
Chris@148 407 }
Chris@148 408
Chris@148 409 if (midiBytesToLong(midiHeader.substr(4,4)) != 6L) {
Chris@148 410 #ifdef MIDI_DEBUG
Chris@690 411 SVDEBUG << "MIDIFileReader::parseHeader()"
Chris@148 412 << " - header length incorrect"
Chris@148 413 << endl;
Chris@148 414 #endif
Chris@148 415 return false;
Chris@148 416 }
Chris@148 417
Chris@148 418 m_format = (MIDIFileFormatType) midiBytesToInt(midiHeader.substr(8,2));
Chris@148 419 m_numberOfTracks = midiBytesToInt(midiHeader.substr(10,2));
Chris@148 420 m_timingDivision = midiBytesToInt(midiHeader.substr(12,2));
Chris@148 421
Chris@613 422 if (m_timingDivision >= 32768) {
Chris@613 423 m_smpte = true;
Chris@613 424 m_fps = 256 - (m_timingDivision >> 8);
Chris@613 425 m_subframes = (m_timingDivision & 0xff);
Chris@613 426 } else {
Chris@613 427 m_smpte = false;
Chris@148 428 }
Chris@148 429
Chris@148 430 return true;
Chris@148 431 }
Chris@148 432
Chris@148 433 // Extract the contents from a MIDI file track and places it into
Chris@148 434 // our local map of MIDI events.
Chris@148 435 //
Chris@148 436 bool
Chris@148 437 MIDIFileReader::parseTrack(unsigned int &lastTrackNum)
Chris@148 438 {
Chris@148 439 MIDIByte midiByte, metaEventCode, data1, data2;
Chris@148 440 MIDIByte eventCode = 0x80;
Chris@148 441 string metaMessage;
Chris@148 442 unsigned int messageLength;
Chris@148 443 unsigned long deltaTime;
Chris@148 444 unsigned long accumulatedTime = 0;
Chris@148 445
Chris@148 446 // The trackNum passed in to this method is the default track for
Chris@148 447 // all events provided they're all on the same channel. If we find
Chris@148 448 // events on more than one channel, we increment trackNum and record
Chris@148 449 // the mapping from channel to trackNum in this channelTrackMap.
Chris@148 450 // We then return the new trackNum by reference so the calling
Chris@148 451 // method knows we've got more tracks than expected.
Chris@148 452
Chris@148 453 // This would be a vector<unsigned int> but we need -1 to indicate
Chris@148 454 // "not yet used"
Chris@148 455 vector<int> channelTrackMap(16, -1);
Chris@148 456
Chris@148 457 // This is used to store the last absolute time found on each track,
Chris@148 458 // allowing us to modify delta-times correctly when separating events
Chris@148 459 // out from one to multiple tracks
Chris@148 460 //
Chris@148 461 map<int, unsigned long> trackTimeMap;
Chris@148 462
Chris@148 463 // Meta-events don't have a channel, so we place them in a fixed
Chris@148 464 // track number instead
Chris@148 465 unsigned int metaTrack = lastTrackNum;
Chris@148 466
Chris@148 467 // Remember the last non-meta status byte (-1 if we haven't seen one)
Chris@148 468 int runningStatus = -1;
Chris@148 469
Chris@148 470 bool firstTrack = true;
Chris@148 471
Chris@148 472 while (!m_midiFile->eof() && (m_trackByteCount > 0)) {
Chris@148 473
Chris@148 474 if (eventCode < 0x80) {
Chris@148 475 #ifdef MIDI_DEBUG
Chris@148 476 cerr << "WARNING: Invalid event code " << eventCode
Chris@148 477 << " in MIDI file" << endl;
Chris@148 478 #endif
Chris@148 479 throw MIDIException(tr("Invalid event code %1 found").arg(int(eventCode)));
Chris@148 480 }
Chris@148 481
Chris@148 482 deltaTime = getNumberFromMIDIBytes();
Chris@148 483
Chris@148 484 #ifdef MIDI_DEBUG
Chris@148 485 cerr << "read delta time " << deltaTime << endl;
Chris@148 486 #endif
Chris@148 487
Chris@148 488 // Get a single byte
Chris@148 489 midiByte = getMIDIByte();
Chris@148 490
Chris@148 491 if (!(midiByte & MIDI_STATUS_BYTE_MASK)) {
Chris@148 492
Chris@148 493 if (runningStatus < 0) {
Chris@148 494 throw MIDIException(tr("Running status used for first event in track"));
Chris@148 495 }
Chris@148 496
Chris@148 497 eventCode = (MIDIByte)runningStatus;
Chris@148 498 data1 = midiByte;
Chris@148 499
Chris@148 500 #ifdef MIDI_DEBUG
Chris@690 501 SVDEBUG << "using running status (byte " << int(midiByte) << " found)" << endl;
Chris@148 502 #endif
Chris@148 503 } else {
Chris@148 504 #ifdef MIDI_DEBUG
Chris@148 505 cerr << "have new event code " << int(midiByte) << endl;
Chris@148 506 #endif
Chris@148 507 eventCode = midiByte;
Chris@148 508 data1 = getMIDIByte();
Chris@148 509 }
Chris@148 510
Chris@148 511 if (eventCode == MIDI_FILE_META_EVENT) {
Chris@148 512
Chris@148 513 metaEventCode = data1;
Chris@148 514 messageLength = getNumberFromMIDIBytes();
Chris@148 515
Chris@148 516 //#ifdef MIDI_DEBUG
Chris@148 517 cerr << "Meta event of type " << int(metaEventCode) << " and " << messageLength << " bytes found, putting on track " << metaTrack << endl;
Chris@148 518 //#endif
Chris@148 519 metaMessage = getMIDIBytes(messageLength);
Chris@148 520
Chris@148 521 long gap = accumulatedTime - trackTimeMap[metaTrack];
Chris@148 522 accumulatedTime += deltaTime;
Chris@148 523 deltaTime += gap;
Chris@148 524 trackTimeMap[metaTrack] = accumulatedTime;
Chris@148 525
Chris@148 526 MIDIEvent *e = new MIDIEvent(deltaTime,
Chris@148 527 MIDI_FILE_META_EVENT,
Chris@148 528 metaEventCode,
Chris@148 529 metaMessage);
Chris@148 530
Chris@148 531 m_midiComposition[metaTrack].push_back(e);
Chris@148 532
Chris@148 533 if (metaEventCode == MIDI_TRACK_NAME) {
Chris@148 534 m_trackNames[metaTrack] = metaMessage.c_str();
Chris@148 535 }
Chris@148 536
Chris@148 537 } else { // non-meta events
Chris@148 538
Chris@148 539 runningStatus = eventCode;
Chris@148 540
Chris@148 541 MIDIEvent *midiEvent;
Chris@148 542
Chris@148 543 int channel = (eventCode & MIDI_CHANNEL_NUM_MASK);
Chris@148 544 if (channelTrackMap[channel] == -1) {
Chris@148 545 if (!firstTrack) ++lastTrackNum;
Chris@148 546 else firstTrack = false;
Chris@148 547 channelTrackMap[channel] = lastTrackNum;
Chris@148 548 }
Chris@148 549
Chris@148 550 unsigned int trackNum = channelTrackMap[channel];
Chris@148 551
Chris@148 552 // accumulatedTime is abs time of last event on any track;
Chris@148 553 // trackTimeMap[trackNum] is that of last event on this track
Chris@148 554
Chris@148 555 long gap = accumulatedTime - trackTimeMap[trackNum];
Chris@148 556 accumulatedTime += deltaTime;
Chris@148 557 deltaTime += gap;
Chris@148 558 trackTimeMap[trackNum] = accumulatedTime;
Chris@148 559
Chris@148 560 switch (eventCode & MIDI_MESSAGE_TYPE_MASK) {
Chris@148 561
Chris@148 562 case MIDI_NOTE_ON:
Chris@148 563 case MIDI_NOTE_OFF:
Chris@148 564 case MIDI_POLY_AFTERTOUCH:
Chris@148 565 case MIDI_CTRL_CHANGE:
Chris@148 566 data2 = getMIDIByte();
Chris@148 567
Chris@148 568 // create and store our event
Chris@148 569 midiEvent = new MIDIEvent(deltaTime, eventCode, data1, data2);
Chris@148 570
Chris@148 571 /*
Chris@148 572 cerr << "MIDI event for channel " << channel << " (track "
Chris@148 573 << trackNum << ")" << endl;
Chris@148 574 midiEvent->print();
Chris@148 575 */
Chris@148 576
Chris@148 577
Chris@148 578 m_midiComposition[trackNum].push_back(midiEvent);
Chris@148 579
Chris@148 580 if (midiEvent->getChannelNumber() == MIDI_PERCUSSION_CHANNEL) {
Chris@148 581 m_percussionTracks.insert(trackNum);
Chris@148 582 }
Chris@148 583
Chris@148 584 break;
Chris@148 585
Chris@148 586 case MIDI_PITCH_BEND:
Chris@148 587 data2 = getMIDIByte();
Chris@148 588
Chris@148 589 // create and store our event
Chris@148 590 midiEvent = new MIDIEvent(deltaTime, eventCode, data1, data2);
Chris@148 591 m_midiComposition[trackNum].push_back(midiEvent);
Chris@148 592 break;
Chris@148 593
Chris@148 594 case MIDI_PROG_CHANGE:
Chris@148 595 case MIDI_CHNL_AFTERTOUCH:
Chris@148 596 // create and store our event
Chris@148 597 midiEvent = new MIDIEvent(deltaTime, eventCode, data1);
Chris@148 598 m_midiComposition[trackNum].push_back(midiEvent);
Chris@148 599 break;
Chris@148 600
Chris@148 601 case MIDI_SYSTEM_EXCLUSIVE:
Chris@148 602 messageLength = getNumberFromMIDIBytes(data1);
Chris@148 603
Chris@148 604 #ifdef MIDI_DEBUG
Chris@148 605 cerr << "SysEx of " << messageLength << " bytes found" << endl;
Chris@148 606 #endif
Chris@148 607
Chris@148 608 metaMessage= getMIDIBytes(messageLength);
Chris@148 609
Chris@148 610 if (MIDIByte(metaMessage[metaMessage.length() - 1]) !=
Chris@148 611 MIDI_END_OF_EXCLUSIVE)
Chris@148 612 {
Chris@148 613 #ifdef MIDI_DEBUG
Chris@690 614 SVDEBUG << "MIDIFileReader::parseTrack() - "
Chris@148 615 << "malformed or unsupported SysEx type"
Chris@148 616 << endl;
Chris@148 617 #endif
Chris@148 618 continue;
Chris@148 619 }
Chris@148 620
Chris@148 621 // chop off the EOX
Chris@148 622 // length fixed by Pedro Lopez-Cabanillas (20030523)
Chris@148 623 //
Chris@148 624 metaMessage = metaMessage.substr(0, metaMessage.length()-1);
Chris@148 625
Chris@148 626 midiEvent = new MIDIEvent(deltaTime,
Chris@148 627 MIDI_SYSTEM_EXCLUSIVE,
Chris@148 628 metaMessage);
Chris@148 629 m_midiComposition[trackNum].push_back(midiEvent);
Chris@148 630 break;
Chris@148 631
Chris@148 632 default:
Chris@148 633 #ifdef MIDI_DEBUG
Chris@690 634 SVDEBUG << "MIDIFileReader::parseTrack()"
Chris@148 635 << " - Unsupported MIDI Event Code: "
Chris@148 636 << (int)eventCode << endl;
Chris@148 637 #endif
Chris@148 638 break;
Chris@148 639 }
Chris@148 640 }
Chris@148 641 }
Chris@148 642
Chris@148 643 if (lastTrackNum > metaTrack) {
Chris@148 644 for (unsigned int track = metaTrack + 1; track <= lastTrackNum; ++track) {
Chris@148 645 m_trackNames[track] = QString("%1 <%2>")
Chris@148 646 .arg(m_trackNames[metaTrack]).arg(track - metaTrack + 1);
Chris@148 647 }
Chris@148 648 }
Chris@148 649
Chris@148 650 return true;
Chris@148 651 }
Chris@148 652
Chris@148 653 // Delete dead NOTE OFF and NOTE ON/Zero Velocity Events after
Chris@148 654 // reading them and modifying their relevant NOTE ONs. Return true
Chris@148 655 // if there are some notes in this track.
Chris@148 656 //
Chris@148 657 bool
Chris@148 658 MIDIFileReader::consolidateNoteOffEvents(unsigned int track)
Chris@148 659 {
Chris@148 660 bool notesOnTrack = false;
Chris@148 661 bool noteOffFound;
Chris@148 662
Chris@148 663 for (MIDITrack::iterator i = m_midiComposition[track].begin();
Chris@965 664 i != m_midiComposition[track].end(); i++) {
Chris@148 665
Chris@148 666 if ((*i)->getMessageType() == MIDI_NOTE_ON && (*i)->getVelocity() > 0) {
Chris@148 667
Chris@148 668 notesOnTrack = true;
Chris@148 669 noteOffFound = false;
Chris@148 670
Chris@148 671 for (MIDITrack::iterator j = i;
Chris@148 672 j != m_midiComposition[track].end(); j++) {
Chris@148 673
Chris@148 674 if (((*j)->getChannelNumber() == (*i)->getChannelNumber()) &&
Chris@148 675 ((*j)->getPitch() == (*i)->getPitch()) &&
Chris@148 676 ((*j)->getMessageType() == MIDI_NOTE_OFF ||
Chris@148 677 ((*j)->getMessageType() == MIDI_NOTE_ON &&
Chris@148 678 (*j)->getVelocity() == 0x00))) {
Chris@148 679
Chris@148 680 (*i)->setDuration((*j)->getTime() - (*i)->getTime());
Chris@148 681
Chris@148 682 delete *j;
Chris@148 683 m_midiComposition[track].erase(j);
Chris@148 684
Chris@148 685 noteOffFound = true;
Chris@148 686 break;
Chris@148 687 }
Chris@148 688 }
Chris@148 689
Chris@148 690 // If no matching NOTE OFF has been found then set
Chris@148 691 // Event duration to length of track
Chris@148 692 //
Chris@148 693 if (!noteOffFound) {
Chris@148 694 MIDITrack::iterator j = m_midiComposition[track].end();
Chris@148 695 --j;
Chris@613 696 (*i)->setDuration((*j)->getTime() - (*i)->getTime());
Chris@148 697 }
Chris@148 698 }
Chris@148 699 }
Chris@148 700
Chris@148 701 return notesOnTrack;
Chris@148 702 }
Chris@148 703
Chris@148 704 // Add any tempo events found in the given track to the global tempo map.
Chris@148 705 //
Chris@148 706 void
Chris@148 707 MIDIFileReader::updateTempoMap(unsigned int track)
Chris@148 708 {
Chris@843 709 cerr << "updateTempoMap for track " << track << " (" << m_midiComposition[track].size() << " events)" << endl;
Chris@148 710
Chris@148 711 for (MIDITrack::iterator i = m_midiComposition[track].begin();
Chris@148 712 i != m_midiComposition[track].end(); ++i) {
Chris@148 713
Chris@148 714 if ((*i)->isMeta() &&
Chris@148 715 (*i)->getMetaEventCode() == MIDI_SET_TEMPO) {
Chris@148 716
Chris@148 717 MIDIByte m0 = (*i)->getMetaMessage()[0];
Chris@148 718 MIDIByte m1 = (*i)->getMetaMessage()[1];
Chris@148 719 MIDIByte m2 = (*i)->getMetaMessage()[2];
Chris@148 720
Chris@148 721 long tempo = (((m0 << 8) + m1) << 8) + m2;
Chris@148 722
Chris@843 723 cerr << "updateTempoMap: have tempo, it's " << tempo << " at " << (*i)->getTime() << endl;
Chris@148 724
Chris@148 725 if (tempo != 0) {
Chris@148 726 double qpm = 60000000.0 / double(tempo);
Chris@148 727 m_tempoMap[(*i)->getTime()] =
Chris@148 728 TempoChange(RealTime::zeroTime, qpm);
Chris@148 729 }
Chris@148 730 }
Chris@148 731 }
Chris@148 732 }
Chris@148 733
Chris@148 734 void
Chris@148 735 MIDIFileReader::calculateTempoTimestamps()
Chris@148 736 {
Chris@148 737 unsigned long lastMIDITime = 0;
Chris@148 738 RealTime lastRealTime = RealTime::zeroTime;
Chris@148 739 double tempo = 120.0;
Chris@148 740 int td = m_timingDivision;
Chris@148 741 if (td == 0) td = 96;
Chris@148 742
Chris@148 743 for (TempoMap::iterator i = m_tempoMap.begin(); i != m_tempoMap.end(); ++i) {
Chris@148 744
Chris@148 745 unsigned long mtime = i->first;
Chris@148 746 unsigned long melapsed = mtime - lastMIDITime;
Chris@148 747 double quarters = double(melapsed) / double(td);
Chris@148 748 double seconds = (60.0 * quarters) / tempo;
Chris@148 749
Chris@148 750 RealTime t = lastRealTime + RealTime::fromSeconds(seconds);
Chris@148 751
Chris@148 752 i->second.first = t;
Chris@148 753
Chris@148 754 lastRealTime = t;
Chris@148 755 lastMIDITime = mtime;
Chris@148 756 tempo = i->second.second;
Chris@148 757 }
Chris@148 758 }
Chris@148 759
Chris@148 760 RealTime
Chris@148 761 MIDIFileReader::getTimeForMIDITime(unsigned long midiTime) const
Chris@148 762 {
Chris@148 763 unsigned long tempoMIDITime = 0;
Chris@148 764 RealTime tempoRealTime = RealTime::zeroTime;
Chris@148 765 double tempo = 120.0;
Chris@148 766
Chris@148 767 TempoMap::const_iterator i = m_tempoMap.lower_bound(midiTime);
Chris@148 768 if (i != m_tempoMap.begin()) {
Chris@148 769 --i;
Chris@148 770 tempoMIDITime = i->first;
Chris@148 771 tempoRealTime = i->second.first;
Chris@148 772 tempo = i->second.second;
Chris@148 773 }
Chris@148 774
Chris@148 775 int td = m_timingDivision;
Chris@148 776 if (td == 0) td = 96;
Chris@148 777
Chris@148 778 unsigned long melapsed = midiTime - tempoMIDITime;
Chris@148 779 double quarters = double(melapsed) / double(td);
Chris@148 780 double seconds = (60.0 * quarters) / tempo;
Chris@148 781
Chris@148 782 /*
Chris@690 783 SVDEBUG << "MIDIFileReader::getTimeForMIDITime(" << midiTime << ")"
Chris@687 784 << endl;
Chris@690 785 SVDEBUG << "timing division = " << td << endl;
Chris@843 786 cerr << "nearest tempo event (of " << m_tempoMap.size() << ") is at " << tempoMIDITime << " ("
Chris@843 787 << tempoRealTime << ")" << endl;
Chris@843 788 cerr << "quarters since then = " << quarters << endl;
Chris@843 789 cerr << "tempo = " << tempo << " quarters per minute" << endl;
Chris@843 790 cerr << "seconds since then = " << seconds << endl;
Chris@690 791 SVDEBUG << "resulting time = " << (tempoRealTime + RealTime::fromSeconds(seconds)) << endl;
Chris@148 792 */
Chris@148 793
Chris@148 794 return tempoRealTime + RealTime::fromSeconds(seconds);
Chris@148 795 }
Chris@148 796
Chris@148 797 Model *
Chris@148 798 MIDIFileReader::load() const
Chris@148 799 {
Chris@148 800 if (!isOK()) return 0;
Chris@148 801
Chris@148 802 if (m_loadableTracks.empty()) {
Chris@392 803 if (m_acquirer) {
Chris@392 804 m_acquirer->showError
Chris@392 805 (tr("MIDI file \"%1\" has no notes in any track").arg(m_path));
Chris@392 806 }
Chris@148 807 return 0;
Chris@148 808 }
Chris@148 809
Chris@148 810 std::set<unsigned int> tracksToLoad;
Chris@148 811
Chris@148 812 if (m_loadableTracks.size() == 1) {
Chris@148 813
Chris@148 814 tracksToLoad.insert(*m_loadableTracks.begin());
Chris@148 815
Chris@148 816 } else {
Chris@148 817
Chris@392 818 QStringList displayNames;
Chris@148 819
Chris@148 820 for (set<unsigned int>::iterator i = m_loadableTracks.begin();
Chris@148 821 i != m_loadableTracks.end(); ++i) {
Chris@148 822
Chris@148 823 unsigned int trackNo = *i;
Chris@148 824 QString label;
Chris@148 825
Chris@148 826 QString perc;
Chris@148 827 if (m_percussionTracks.find(trackNo) != m_percussionTracks.end()) {
Chris@148 828 perc = tr(" - uses GM percussion channel");
Chris@148 829 }
Chris@148 830
Chris@148 831 if (m_trackNames.find(trackNo) != m_trackNames.end()) {
Chris@148 832 label = tr("Track %1 (%2)%3")
Chris@148 833 .arg(trackNo).arg(m_trackNames.find(trackNo)->second)
Chris@148 834 .arg(perc);
Chris@148 835 } else {
Chris@148 836 label = tr("Track %1 (untitled)%3").arg(trackNo).arg(perc);
Chris@148 837 }
Chris@392 838
Chris@392 839 displayNames << label;
Chris@148 840 }
Chris@148 841
Chris@392 842 QString singleTrack;
Chris@148 843
Chris@392 844 bool haveSomePercussion =
Chris@392 845 (!m_percussionTracks.empty() &&
Chris@392 846 (m_percussionTracks.size() < m_loadableTracks.size()));
Chris@148 847
Chris@392 848 MIDIFileImportPreferenceAcquirer::TrackPreference pref;
Chris@392 849
Chris@392 850 if (m_acquirer) {
Chris@392 851 pref = m_acquirer->getTrackImportPreference(displayNames,
Chris@392 852 haveSomePercussion,
Chris@392 853 singleTrack);
Chris@392 854 } else {
Chris@392 855 pref = MIDIFileImportPreferenceAcquirer::MergeAllTracks;
Chris@392 856 }
Chris@392 857
Chris@392 858 if (pref == MIDIFileImportPreferenceAcquirer::ImportNothing) return 0;
Chris@392 859
Chris@392 860 if (pref == MIDIFileImportPreferenceAcquirer::MergeAllTracks ||
Chris@392 861 pref == MIDIFileImportPreferenceAcquirer::MergeAllNonPercussionTracks) {
Chris@392 862
Chris@392 863 for (set<unsigned int>::iterator i = m_loadableTracks.begin();
Chris@392 864 i != m_loadableTracks.end(); ++i) {
Chris@392 865
Chris@392 866 if (pref == MIDIFileImportPreferenceAcquirer::MergeAllTracks ||
Chris@148 867 m_percussionTracks.find(*i) == m_percussionTracks.end()) {
Chris@392 868
Chris@148 869 tracksToLoad.insert(*i);
Chris@148 870 }
Chris@148 871 }
Chris@148 872
Chris@148 873 } else {
Chris@148 874
Chris@392 875 int j = 0;
Chris@148 876
Chris@148 877 for (set<unsigned int>::iterator i = m_loadableTracks.begin();
Chris@148 878 i != m_loadableTracks.end(); ++i) {
Chris@148 879
Chris@392 880 if (singleTrack == displayNames[j]) {
Chris@148 881 tracksToLoad.insert(*i);
Chris@148 882 break;
Chris@148 883 }
Chris@148 884
Chris@148 885 ++j;
Chris@148 886 }
Chris@148 887 }
Chris@148 888 }
Chris@148 889
Chris@148 890 if (tracksToLoad.empty()) return 0;
Chris@148 891
Chris@929 892 int n = tracksToLoad.size(), count = 0;
Chris@148 893 Model *model = 0;
Chris@148 894
Chris@148 895 for (std::set<unsigned int>::iterator i = tracksToLoad.begin();
Chris@148 896 i != tracksToLoad.end(); ++i) {
Chris@148 897
Chris@148 898 int minProgress = (100 * count) / n;
Chris@148 899 int progressAmount = 100 / n;
Chris@148 900
Chris@148 901 model = loadTrack(*i, model, minProgress, progressAmount);
Chris@148 902
Chris@148 903 ++count;
Chris@148 904 }
Chris@148 905
Chris@148 906 if (dynamic_cast<NoteModel *>(model)) {
Chris@148 907 dynamic_cast<NoteModel *>(model)->setCompletion(100);
Chris@148 908 }
Chris@148 909
Chris@148 910 return model;
Chris@148 911 }
Chris@148 912
Chris@148 913 Model *
Chris@148 914 MIDIFileReader::loadTrack(unsigned int trackToLoad,
Chris@148 915 Model *existingModel,
Chris@148 916 int minProgress,
Chris@148 917 int progressAmount) const
Chris@148 918 {
Chris@148 919 if (m_midiComposition.find(trackToLoad) == m_midiComposition.end()) {
Chris@148 920 return 0;
Chris@148 921 }
Chris@148 922
Chris@148 923 NoteModel *model = 0;
Chris@148 924
Chris@148 925 if (existingModel) {
Chris@148 926 model = dynamic_cast<NoteModel *>(existingModel);
Chris@148 927 if (!model) {
Chris@843 928 cerr << "WARNING: MIDIFileReader::loadTrack: Existing model given, but it isn't a NoteModel -- ignoring it" << endl;
Chris@148 929 }
Chris@148 930 }
Chris@148 931
Chris@148 932 if (!model) {
Chris@148 933 model = new NoteModel(m_mainModelSampleRate, 1, 0.0, 0.0, false);
Chris@148 934 model->setValueQuantization(1.0);
Chris@148 935 }
Chris@148 936
Chris@148 937 const MIDITrack &track = m_midiComposition.find(trackToLoad)->second;
Chris@148 938
Chris@929 939 int totalEvents = track.size();
Chris@929 940 int count = 0;
Chris@148 941
Chris@148 942 bool sharpKey = true;
Chris@148 943
Chris@148 944 for (MIDITrack::const_iterator i = track.begin(); i != track.end(); ++i) {
Chris@148 945
Chris@613 946 RealTime rt;
Chris@613 947 unsigned long midiTime = (*i)->getTime();
Chris@613 948
Chris@613 949 if (m_smpte) {
Chris@613 950 rt = RealTime::frame2RealTime(midiTime, m_fps * m_subframes);
Chris@613 951 } else {
Chris@613 952 rt = getTimeForMIDITime(midiTime);
Chris@613 953 }
Chris@148 954
Chris@148 955 // We ignore most of these event types for now, though in
Chris@148 956 // theory some of the text ones could usefully be incorporated
Chris@148 957
Chris@148 958 if ((*i)->isMeta()) {
Chris@148 959
Chris@148 960 switch((*i)->getMetaEventCode()) {
Chris@148 961
Chris@148 962 case MIDI_KEY_SIGNATURE:
Chris@929 963 // minorKey = (int((*i)->getMetaMessage()[1]) != 0);
Chris@148 964 sharpKey = (int((*i)->getMetaMessage()[0]) >= 0);
Chris@148 965 break;
Chris@148 966
Chris@148 967 case MIDI_TEXT_EVENT:
Chris@148 968 case MIDI_LYRIC:
Chris@148 969 case MIDI_TEXT_MARKER:
Chris@148 970 case MIDI_COPYRIGHT_NOTICE:
Chris@148 971 case MIDI_TRACK_NAME:
Chris@148 972 // The text events that we could potentially use
Chris@148 973 break;
Chris@148 974
Chris@148 975 case MIDI_SET_TEMPO:
Chris@148 976 // Already dealt with in a separate pass previously
Chris@148 977 break;
Chris@148 978
Chris@148 979 case MIDI_TIME_SIGNATURE:
Chris@148 980 // Not yet!
Chris@148 981 break;
Chris@148 982
Chris@148 983 case MIDI_SEQUENCE_NUMBER:
Chris@148 984 case MIDI_CHANNEL_PREFIX_OR_PORT:
Chris@148 985 case MIDI_INSTRUMENT_NAME:
Chris@148 986 case MIDI_CUE_POINT:
Chris@148 987 case MIDI_CHANNEL_PREFIX:
Chris@148 988 case MIDI_SEQUENCER_SPECIFIC:
Chris@148 989 case MIDI_SMPTE_OFFSET:
Chris@148 990 default:
Chris@148 991 break;
Chris@148 992 }
Chris@148 993
Chris@148 994 } else {
Chris@148 995
Chris@148 996 switch ((*i)->getMessageType()) {
Chris@148 997
Chris@148 998 case MIDI_NOTE_ON:
Chris@148 999
Chris@148 1000 if ((*i)->getVelocity() == 0) break; // effective note-off
Chris@148 1001 else {
Chris@613 1002 RealTime endRT;
Chris@613 1003 unsigned long endMidiTime = (*i)->getTime() + (*i)->getDuration();
Chris@613 1004 if (m_smpte) {
Chris@613 1005 endRT = RealTime::frame2RealTime(endMidiTime, m_fps * m_subframes);
Chris@613 1006 } else {
Chris@613 1007 endRT = getTimeForMIDITime(endMidiTime);
Chris@613 1008 }
Chris@148 1009
Chris@148 1010 long startFrame = RealTime::realTime2Frame
Chris@148 1011 (rt, model->getSampleRate());
Chris@148 1012
Chris@148 1013 long endFrame = RealTime::realTime2Frame
Chris@148 1014 (endRT, model->getSampleRate());
Chris@148 1015
Chris@148 1016 QString pitchLabel = Pitch::getPitchLabel((*i)->getPitch(),
Chris@148 1017 0,
Chris@148 1018 !sharpKey);
Chris@148 1019
Chris@148 1020 QString noteLabel = tr("%1 - vel %2")
Chris@148 1021 .arg(pitchLabel).arg(int((*i)->getVelocity()));
Chris@148 1022
Chris@340 1023 float level = float((*i)->getVelocity()) / 128.f;
Chris@340 1024
Chris@148 1025 Note note(startFrame, (*i)->getPitch(),
Chris@340 1026 endFrame - startFrame, level, noteLabel);
Chris@148 1027
Chris@690 1028 // SVDEBUG << "Adding note " << startFrame << "," << (endFrame-startFrame) << " : " << int((*i)->getPitch()) << endl;
Chris@148 1029
Chris@148 1030 model->addPoint(note);
Chris@148 1031 break;
Chris@148 1032 }
Chris@148 1033
Chris@148 1034 case MIDI_PITCH_BEND:
Chris@148 1035 // I guess we could make some use of this...
Chris@148 1036 break;
Chris@148 1037
Chris@148 1038 case MIDI_NOTE_OFF:
Chris@148 1039 case MIDI_PROG_CHANGE:
Chris@148 1040 case MIDI_CTRL_CHANGE:
Chris@148 1041 case MIDI_SYSTEM_EXCLUSIVE:
Chris@148 1042 case MIDI_POLY_AFTERTOUCH:
Chris@148 1043 case MIDI_CHNL_AFTERTOUCH:
Chris@148 1044 break;
Chris@148 1045
Chris@148 1046 default:
Chris@148 1047 break;
Chris@148 1048 }
Chris@148 1049 }
Chris@148 1050
Chris@148 1051 model->setCompletion(minProgress +
Chris@148 1052 (count * progressAmount) / totalEvents);
Chris@148 1053 ++count;
Chris@148 1054 }
Chris@148 1055
Chris@148 1056 return model;
Chris@148 1057 }
Chris@148 1058
Chris@148 1059