annotate data/fileio/MIDIFileReader.cpp @ 308:14e0f60435b8

* Make it possible to drop audio files, layer files, session files and images onto SV panes. Need to do a bit more work on where we expect the dropped file to go, particularly in the case of audio files -- at the moment they're always opened in new panes, but it may be better to by default replace whatever is in the target pane.
author Chris Cannam
date Wed, 10 Oct 2007 15:18:02 +0000
parents 73537d900d4b
children 516819f2b97b
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@301 31 #include "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 #include <QMessageBox>
Chris@148 40 #include <QInputDialog>
Chris@148 41
Chris@148 42 #include <sstream>
Chris@148 43
Chris@148 44 using std::string;
Chris@148 45 using std::ifstream;
Chris@148 46 using std::stringstream;
Chris@148 47 using std::cerr;
Chris@148 48 using std::endl;
Chris@148 49 using std::ends;
Chris@148 50 using std::ios;
Chris@148 51 using std::vector;
Chris@148 52 using std::map;
Chris@148 53 using std::set;
Chris@148 54
Chris@301 55 using namespace MIDIConstants;
Chris@301 56
Chris@148 57 //#define MIDI_DEBUG 1
Chris@148 58
Chris@148 59
Chris@148 60 MIDIFileReader::MIDIFileReader(QString path,
Chris@148 61 size_t mainModelSampleRate) :
Chris@148 62 m_timingDivision(0),
Chris@148 63 m_format(MIDI_FILE_BAD_FORMAT),
Chris@148 64 m_numberOfTracks(0),
Chris@148 65 m_trackByteCount(0),
Chris@148 66 m_decrementCount(false),
Chris@148 67 m_path(path),
Chris@148 68 m_midiFile(0),
Chris@148 69 m_fileSize(0),
Chris@148 70 m_mainModelSampleRate(mainModelSampleRate)
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@148 816 QMessageBox::critical(0, tr("No notes in MIDI file"),
Chris@148 817 tr("MIDI file \"%1\" has no notes in any track")
Chris@148 818 .arg(m_path));
Chris@148 819 return 0;
Chris@148 820 }
Chris@148 821
Chris@148 822 std::set<unsigned int> tracksToLoad;
Chris@148 823
Chris@148 824 if (m_loadableTracks.size() == 1) {
Chris@148 825
Chris@148 826 tracksToLoad.insert(*m_loadableTracks.begin());
Chris@148 827
Chris@148 828 } else {
Chris@148 829
Chris@148 830 QStringList available;
Chris@148 831 QString allTracks = tr("Merge all tracks");
Chris@148 832 QString allNonPercussion = tr("Merge all non-percussion tracks");
Chris@148 833
Chris@148 834 int nonTrackItems = 1;
Chris@148 835
Chris@148 836 available << allTracks;
Chris@148 837
Chris@148 838 if (!m_percussionTracks.empty() &&
Chris@148 839 (m_percussionTracks.size() < m_loadableTracks.size())) {
Chris@148 840 available << allNonPercussion;
Chris@148 841 ++nonTrackItems;
Chris@148 842 }
Chris@148 843
Chris@148 844 for (set<unsigned int>::iterator i = m_loadableTracks.begin();
Chris@148 845 i != m_loadableTracks.end(); ++i) {
Chris@148 846
Chris@148 847 unsigned int trackNo = *i;
Chris@148 848 QString label;
Chris@148 849
Chris@148 850 QString perc;
Chris@148 851 if (m_percussionTracks.find(trackNo) != m_percussionTracks.end()) {
Chris@148 852 perc = tr(" - uses GM percussion channel");
Chris@148 853 }
Chris@148 854
Chris@148 855 if (m_trackNames.find(trackNo) != m_trackNames.end()) {
Chris@148 856 label = tr("Track %1 (%2)%3")
Chris@148 857 .arg(trackNo).arg(m_trackNames.find(trackNo)->second)
Chris@148 858 .arg(perc);
Chris@148 859 } else {
Chris@148 860 label = tr("Track %1 (untitled)%3").arg(trackNo).arg(perc);
Chris@148 861 }
Chris@148 862 available << label;
Chris@148 863 }
Chris@148 864
Chris@148 865 bool ok = false;
Chris@148 866 QString selected = QInputDialog::getItem
Chris@148 867 (0, tr("Select track or tracks to import"),
Chris@308 868 tr("<b>Select track to import</b><p>You can only import this file as a single annotation layer, but the file contains more than one track, or notes on more than one channel.<p>Please select the track or merged tracks you wish to import:"),
Chris@148 869 available, 0, false, &ok);
Chris@148 870
Chris@148 871 if (!ok || selected.isEmpty()) return 0;
Chris@148 872
Chris@148 873 if (selected == allTracks || selected == allNonPercussion) {
Chris@148 874
Chris@148 875 for (set<unsigned int>::iterator i = m_loadableTracks.begin();
Chris@148 876 i != m_loadableTracks.end(); ++i) {
Chris@148 877
Chris@148 878 if (selected == allTracks ||
Chris@148 879 m_percussionTracks.find(*i) == m_percussionTracks.end()) {
Chris@148 880
Chris@148 881 tracksToLoad.insert(*i);
Chris@148 882 }
Chris@148 883 }
Chris@148 884
Chris@148 885 } else {
Chris@148 886
Chris@148 887 int j = nonTrackItems;
Chris@148 888
Chris@148 889 for (set<unsigned int>::iterator i = m_loadableTracks.begin();
Chris@148 890 i != m_loadableTracks.end(); ++i) {
Chris@148 891
Chris@148 892 if (selected == available[j]) {
Chris@148 893 tracksToLoad.insert(*i);
Chris@148 894 break;
Chris@148 895 }
Chris@148 896
Chris@148 897 ++j;
Chris@148 898 }
Chris@148 899 }
Chris@148 900 }
Chris@148 901
Chris@148 902 if (tracksToLoad.empty()) return 0;
Chris@148 903
Chris@148 904 size_t n = tracksToLoad.size(), count = 0;
Chris@148 905 Model *model = 0;
Chris@148 906
Chris@148 907 for (std::set<unsigned int>::iterator i = tracksToLoad.begin();
Chris@148 908 i != tracksToLoad.end(); ++i) {
Chris@148 909
Chris@148 910 int minProgress = (100 * count) / n;
Chris@148 911 int progressAmount = 100 / n;
Chris@148 912
Chris@148 913 model = loadTrack(*i, model, minProgress, progressAmount);
Chris@148 914
Chris@148 915 ++count;
Chris@148 916 }
Chris@148 917
Chris@148 918 if (dynamic_cast<NoteModel *>(model)) {
Chris@148 919 dynamic_cast<NoteModel *>(model)->setCompletion(100);
Chris@148 920 }
Chris@148 921
Chris@148 922 return model;
Chris@148 923 }
Chris@148 924
Chris@148 925 Model *
Chris@148 926 MIDIFileReader::loadTrack(unsigned int trackToLoad,
Chris@148 927 Model *existingModel,
Chris@148 928 int minProgress,
Chris@148 929 int progressAmount) const
Chris@148 930 {
Chris@148 931 if (m_midiComposition.find(trackToLoad) == m_midiComposition.end()) {
Chris@148 932 return 0;
Chris@148 933 }
Chris@148 934
Chris@148 935 NoteModel *model = 0;
Chris@148 936
Chris@148 937 if (existingModel) {
Chris@148 938 model = dynamic_cast<NoteModel *>(existingModel);
Chris@148 939 if (!model) {
Chris@148 940 std::cerr << "WARNING: MIDIFileReader::loadTrack: Existing model given, but it isn't a NoteModel -- ignoring it" << std::endl;
Chris@148 941 }
Chris@148 942 }
Chris@148 943
Chris@148 944 if (!model) {
Chris@148 945 model = new NoteModel(m_mainModelSampleRate, 1, 0.0, 0.0, false);
Chris@148 946 model->setValueQuantization(1.0);
Chris@148 947 }
Chris@148 948
Chris@148 949 const MIDITrack &track = m_midiComposition.find(trackToLoad)->second;
Chris@148 950
Chris@148 951 size_t totalEvents = track.size();
Chris@148 952 size_t count = 0;
Chris@148 953
Chris@148 954 bool minorKey = false;
Chris@148 955 bool sharpKey = true;
Chris@148 956
Chris@148 957 for (MIDITrack::const_iterator i = track.begin(); i != track.end(); ++i) {
Chris@148 958
Chris@148 959 RealTime rt = getTimeForMIDITime((*i)->getTime());
Chris@148 960
Chris@148 961 // We ignore most of these event types for now, though in
Chris@148 962 // theory some of the text ones could usefully be incorporated
Chris@148 963
Chris@148 964 if ((*i)->isMeta()) {
Chris@148 965
Chris@148 966 switch((*i)->getMetaEventCode()) {
Chris@148 967
Chris@148 968 case MIDI_KEY_SIGNATURE:
Chris@148 969 minorKey = (int((*i)->getMetaMessage()[1]) != 0);
Chris@148 970 sharpKey = (int((*i)->getMetaMessage()[0]) >= 0);
Chris@148 971 break;
Chris@148 972
Chris@148 973 case MIDI_TEXT_EVENT:
Chris@148 974 case MIDI_LYRIC:
Chris@148 975 case MIDI_TEXT_MARKER:
Chris@148 976 case MIDI_COPYRIGHT_NOTICE:
Chris@148 977 case MIDI_TRACK_NAME:
Chris@148 978 // The text events that we could potentially use
Chris@148 979 break;
Chris@148 980
Chris@148 981 case MIDI_SET_TEMPO:
Chris@148 982 // Already dealt with in a separate pass previously
Chris@148 983 break;
Chris@148 984
Chris@148 985 case MIDI_TIME_SIGNATURE:
Chris@148 986 // Not yet!
Chris@148 987 break;
Chris@148 988
Chris@148 989 case MIDI_SEQUENCE_NUMBER:
Chris@148 990 case MIDI_CHANNEL_PREFIX_OR_PORT:
Chris@148 991 case MIDI_INSTRUMENT_NAME:
Chris@148 992 case MIDI_CUE_POINT:
Chris@148 993 case MIDI_CHANNEL_PREFIX:
Chris@148 994 case MIDI_SEQUENCER_SPECIFIC:
Chris@148 995 case MIDI_SMPTE_OFFSET:
Chris@148 996 default:
Chris@148 997 break;
Chris@148 998 }
Chris@148 999
Chris@148 1000 } else {
Chris@148 1001
Chris@148 1002 switch ((*i)->getMessageType()) {
Chris@148 1003
Chris@148 1004 case MIDI_NOTE_ON:
Chris@148 1005
Chris@148 1006 if ((*i)->getVelocity() == 0) break; // effective note-off
Chris@148 1007 else {
Chris@148 1008 RealTime endRT = getTimeForMIDITime((*i)->getTime() +
Chris@148 1009 (*i)->getDuration());
Chris@148 1010
Chris@148 1011 long startFrame = RealTime::realTime2Frame
Chris@148 1012 (rt, model->getSampleRate());
Chris@148 1013
Chris@148 1014 long endFrame = RealTime::realTime2Frame
Chris@148 1015 (endRT, model->getSampleRate());
Chris@148 1016
Chris@148 1017 QString pitchLabel = Pitch::getPitchLabel((*i)->getPitch(),
Chris@148 1018 0,
Chris@148 1019 !sharpKey);
Chris@148 1020
Chris@148 1021 QString noteLabel = tr("%1 - vel %2")
Chris@148 1022 .arg(pitchLabel).arg(int((*i)->getVelocity()));
Chris@148 1023
Chris@148 1024 Note note(startFrame, (*i)->getPitch(),
Chris@148 1025 endFrame - startFrame, noteLabel);
Chris@148 1026
Chris@148 1027 // std::cerr << "Adding note " << startFrame << "," << (endFrame-startFrame) << " : " << int((*i)->getPitch()) << std::endl;
Chris@148 1028
Chris@148 1029 model->addPoint(note);
Chris@148 1030 break;
Chris@148 1031 }
Chris@148 1032
Chris@148 1033 case MIDI_PITCH_BEND:
Chris@148 1034 // I guess we could make some use of this...
Chris@148 1035 break;
Chris@148 1036
Chris@148 1037 case MIDI_NOTE_OFF:
Chris@148 1038 case MIDI_PROG_CHANGE:
Chris@148 1039 case MIDI_CTRL_CHANGE:
Chris@148 1040 case MIDI_SYSTEM_EXCLUSIVE:
Chris@148 1041 case MIDI_POLY_AFTERTOUCH:
Chris@148 1042 case MIDI_CHNL_AFTERTOUCH:
Chris@148 1043 break;
Chris@148 1044
Chris@148 1045 default:
Chris@148 1046 break;
Chris@148 1047 }
Chris@148 1048 }
Chris@148 1049
Chris@148 1050 model->setCompletion(minProgress +
Chris@148 1051 (count * progressAmount) / totalEvents);
Chris@148 1052 ++count;
Chris@148 1053 }
Chris@148 1054
Chris@148 1055 return model;
Chris@148 1056 }
Chris@148 1057
Chris@148 1058