annotate data/fileio/MIDIFileReader.cpp @ 588:d04b8674b710

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