| Chris@147 | 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */ | 
| Chris@147 | 2 | 
| Chris@147 | 3 /* | 
| Chris@147 | 4     Sonic Visualiser | 
| Chris@147 | 5     An audio file viewer and annotation editor. | 
| Chris@147 | 6     Centre for Digital Music, Queen Mary, University of London. | 
| Chris@202 | 7     This file copyright 2006 Chris Cannam and QMUL. | 
| Chris@147 | 8 | 
| Chris@147 | 9     This program is free software; you can redistribute it and/or | 
| Chris@147 | 10     modify it under the terms of the GNU General Public License as | 
| Chris@147 | 11     published by the Free Software Foundation; either version 2 of the | 
| Chris@147 | 12     License, or (at your option) any later version.  See the file | 
| Chris@147 | 13     COPYING included with this distribution for more information. | 
| Chris@147 | 14 */ | 
| Chris@147 | 15 | 
| Chris@1122 | 16 #include "ReadOnlyWaveFileModel.h" | 
| Chris@147 | 17 | 
| Chris@147 | 18 #include "fileio/AudioFileReader.h" | 
| Chris@147 | 19 #include "fileio/AudioFileReaderFactory.h" | 
| Chris@147 | 20 | 
| Chris@150 | 21 #include "system/System.h" | 
| Chris@147 | 22 | 
| Chris@921 | 23 #include "base/Preferences.h" | 
| Chris@1751 | 24 #include "base/PlayParameterRepository.h" | 
| Chris@921 | 25 | 
| Chris@147 | 26 #include <QFileInfo> | 
| Chris@314 | 27 #include <QTextStream> | 
| Chris@147 | 28 | 
| Chris@147 | 29 #include <iostream> | 
| Chris@572 | 30 #include <cmath> | 
| Chris@147 | 31 #include <sndfile.h> | 
| Chris@147 | 32 | 
| Chris@147 | 33 #include <cassert> | 
| Chris@147 | 34 | 
| Chris@1096 | 35 using namespace std; | 
| Chris@1096 | 36 | 
| Chris@236 | 37 //#define DEBUG_WAVE_FILE_MODEL 1 | 
| Chris@1809 | 38 //#define DEBUG_WAVE_FILE_MODEL_READ 1 | 
| Chris@236 | 39 | 
| Chris@179 | 40 PowerOfSqrtTwoZoomConstraint | 
| Chris@1122 | 41 ReadOnlyWaveFileModel::m_zoomConstraint; | 
| Chris@179 | 42 | 
| Chris@1122 | 43 ReadOnlyWaveFileModel::ReadOnlyWaveFileModel(FileSource source, sv_samplerate_t targetRate) : | 
| Chris@316 | 44     m_source(source), | 
| Chris@316 | 45     m_path(source.getLocation()), | 
| Chris@1582 | 46     m_reader(nullptr), | 
| Chris@175 | 47     m_myReader(true), | 
| Chris@300 | 48     m_startFrame(0), | 
| Chris@1582 | 49     m_fillThread(nullptr), | 
| Chris@1582 | 50     m_updateTimer(nullptr), | 
| Chris@147 | 51     m_lastFillExtent(0), | 
| Chris@1557 | 52     m_prevCompletion(0), | 
| Chris@752 | 53     m_exiting(false), | 
| Chris@752 | 54     m_lastDirectReadStart(0), | 
| Chris@752 | 55     m_lastDirectReadCount(0) | 
| Chris@147 | 56 { | 
| Chris@1557 | 57     SVDEBUG << "ReadOnlyWaveFileModel::ReadOnlyWaveFileModel: path " | 
| Chris@1557 | 58             << m_path << ", target rate " << targetRate << endl; | 
| Chris@1557 | 59 | 
| Chris@316 | 60     m_source.waitForData(); | 
| Chris@1313 | 61 | 
| Chris@316 | 62     if (m_source.isOK()) { | 
| Chris@1313 | 63 | 
| Chris@1313 | 64         Preferences *prefs = Preferences::getInstance(); | 
| Chris@1313 | 65 | 
| Chris@1313 | 66         AudioFileReaderFactory::Parameters params; | 
| Chris@1313 | 67         params.targetRate = targetRate; | 
| Chris@1313 | 68 | 
| Chris@1313 | 69         params.normalisation = prefs->getNormaliseAudio() ? | 
| Chris@1313 | 70             AudioFileReaderFactory::Normalisation::Peak : | 
| Chris@1313 | 71             AudioFileReaderFactory::Normalisation::None; | 
| Chris@1313 | 72 | 
| Chris@1313 | 73         params.gaplessMode = prefs->getUseGaplessMode() ? | 
| Chris@1313 | 74             AudioFileReaderFactory::GaplessMode::Gapless : | 
| Chris@1313 | 75             AudioFileReaderFactory::GaplessMode::Gappy; | 
| Chris@1313 | 76 | 
| Chris@1313 | 77         params.threadingMode = AudioFileReaderFactory::ThreadingMode::Threaded; | 
| Chris@1313 | 78 | 
| Chris@1313 | 79         m_reader = AudioFileReaderFactory::createReader(m_source, params); | 
| Chris@316 | 80         if (m_reader) { | 
| Chris@1122 | 81             SVDEBUG << "ReadOnlyWaveFileModel::ReadOnlyWaveFileModel: reader rate: " | 
| Chris@687 | 82                       << m_reader->getSampleRate() << endl; | 
| Chris@316 | 83         } | 
| Chris@316 | 84     } | 
| Chris@1557 | 85 | 
| Chris@292 | 86     if (m_reader) setObjectName(m_reader->getTitle()); | 
| Chris@316 | 87     if (objectName() == "") setObjectName(QFileInfo(m_path).fileName()); | 
| Chris@175 | 88     if (isOK()) fillCache(); | 
| Chris@1751 | 89 | 
| Chris@1751 | 90     PlayParameterRepository::getInstance()->addPlayable | 
| Chris@1751 | 91         (getId().untyped, this); | 
| Chris@175 | 92 } | 
| Chris@175 | 93 | 
| Chris@1122 | 94 ReadOnlyWaveFileModel::ReadOnlyWaveFileModel(FileSource source, AudioFileReader *reader) : | 
| Chris@316 | 95     m_source(source), | 
| Chris@316 | 96     m_path(source.getLocation()), | 
| Chris@1582 | 97     m_reader(nullptr), | 
| Chris@175 | 98     m_myReader(false), | 
| Chris@300 | 99     m_startFrame(0), | 
| Chris@1582 | 100     m_fillThread(nullptr), | 
| Chris@1582 | 101     m_updateTimer(nullptr), | 
| Chris@175 | 102     m_lastFillExtent(0), | 
| Chris@1557 | 103     m_prevCompletion(0), | 
| Chris@175 | 104     m_exiting(false) | 
| Chris@175 | 105 { | 
| Chris@1557 | 106     SVDEBUG << "ReadOnlyWaveFileModel::ReadOnlyWaveFileModel: path " | 
| Chris@1557 | 107             << m_path << ", with reader" << endl; | 
| Chris@1557 | 108 | 
| Chris@175 | 109     m_reader = reader; | 
| Chris@292 | 110     if (m_reader) setObjectName(m_reader->getTitle()); | 
| Chris@316 | 111     if (objectName() == "") setObjectName(QFileInfo(m_path).fileName()); | 
| Chris@187 | 112     fillCache(); | 
| Chris@1751 | 113 | 
| Chris@1751 | 114     PlayParameterRepository::getInstance()->addPlayable | 
| Chris@1751 | 115         (getId().untyped, this); | 
| Chris@147 | 116 } | 
| Chris@147 | 117 | 
| Chris@1122 | 118 ReadOnlyWaveFileModel::~ReadOnlyWaveFileModel() | 
| Chris@147 | 119 { | 
| Chris@1751 | 120     PlayParameterRepository::getInstance()->removePlayable | 
| Chris@1751 | 121         (getId().untyped); | 
| Chris@1751 | 122 | 
| Chris@147 | 123     m_exiting = true; | 
| Chris@147 | 124     if (m_fillThread) m_fillThread->wait(); | 
| Chris@175 | 125     if (m_myReader) delete m_reader; | 
| Chris@1582 | 126     m_reader = nullptr; | 
| Chris@1404 | 127 | 
| Chris@1404 | 128     SVDEBUG << "ReadOnlyWaveFileModel: Destructor exiting; we had caches of " | 
| Chris@1404 | 129             << (m_cache[0].size() * sizeof(Range)) << " and " | 
| Chris@1404 | 130             << (m_cache[1].size() * sizeof(Range)) << " bytes" << endl; | 
| Chris@147 | 131 } | 
| Chris@147 | 132 | 
| Chris@147 | 133 bool | 
| Chris@1122 | 134 ReadOnlyWaveFileModel::isOK() const | 
| Chris@147 | 135 { | 
| Chris@147 | 136     return m_reader && m_reader->isOK(); | 
| Chris@147 | 137 } | 
| Chris@147 | 138 | 
| Chris@147 | 139 bool | 
| Chris@1122 | 140 ReadOnlyWaveFileModel::isReady(int *completion) const | 
| Chris@147 | 141 { | 
| Chris@1557 | 142     bool ready = true; | 
| Chris@1557 | 143     if (!isOK()) ready = false; | 
| Chris@1557 | 144     if (m_fillThread) ready = false; | 
| Chris@1557 | 145     if (m_reader && m_reader->isUpdating()) ready = false; | 
| Chris@1557 | 146 | 
| Chris@1557 | 147     double c = double(m_lastFillExtent) / | 
| Chris@1557 | 148         double(getEndFrame() - getStartFrame()); | 
| Chris@1557 | 149 | 
| Chris@265 | 150     if (completion) { | 
| Chris@265 | 151         *completion = int(c * 100.0 + 0.01); | 
| Chris@265 | 152         if (m_reader) { | 
| Chris@265 | 153             int decodeCompletion = m_reader->getDecodeCompletion(); | 
| Chris@266 | 154             if (decodeCompletion < 90) *completion = decodeCompletion; | 
| Chris@1096 | 155             else *completion = min(*completion, decodeCompletion); | 
| Chris@265 | 156         } | 
| Chris@266 | 157         if (*completion != 0 && | 
| Chris@266 | 158             *completion != 100 && | 
| Chris@1557 | 159             m_prevCompletion != 0 && | 
| Chris@1557 | 160             m_prevCompletion > *completion) { | 
| Chris@266 | 161             // just to avoid completion going backwards | 
| Chris@1557 | 162             *completion = m_prevCompletion; | 
| Chris@266 | 163         } | 
| Chris@1557 | 164         m_prevCompletion = *completion; | 
| Chris@265 | 165     } | 
| Chris@1557 | 166 | 
| Chris@236 | 167 #ifdef DEBUG_WAVE_FILE_MODEL | 
| Chris@1557 | 168     if (completion) { | 
| Chris@1809 | 169         SVCERR << "ReadOnlyWaveFileModel(" << objectName() << ")::isReady(): ready = " << ready << ", m_fillThread = " << m_fillThread << ", m_lastFillExtent = " << m_lastFillExtent << ", end frame = " << getEndFrame() << ", start frame = " << getStartFrame() << ", c = " << c << ", completion = " << *completion << endl; | 
| Chris@1557 | 170     } else { | 
| Chris@1809 | 171         SVCERR << "ReadOnlyWaveFileModel(" << objectName() << ")::isReady(): ready = " << ready << ", m_fillThread = " << m_fillThread << ", m_lastFillExtent = " << m_lastFillExtent << ", end frame = " << getEndFrame() << ", start frame = " << getStartFrame() << ", c = " << c << ", completion not requested" << endl; | 
| Chris@1557 | 172     } | 
| Chris@236 | 173 #endif | 
| Chris@147 | 174     return ready; | 
| Chris@147 | 175 } | 
| Chris@147 | 176 | 
| Chris@1038 | 177 sv_frame_t | 
| Chris@1122 | 178 ReadOnlyWaveFileModel::getFrameCount() const | 
| Chris@147 | 179 { | 
| Chris@147 | 180     if (!m_reader) return 0; | 
| Chris@147 | 181     return m_reader->getFrameCount(); | 
| Chris@147 | 182 } | 
| Chris@147 | 183 | 
| Chris@929 | 184 int | 
| Chris@1122 | 185 ReadOnlyWaveFileModel::getChannelCount() const | 
| Chris@147 | 186 { | 
| Chris@147 | 187     if (!m_reader) return 0; | 
| Chris@147 | 188     return m_reader->getChannelCount(); | 
| Chris@147 | 189 } | 
| Chris@147 | 190 | 
| Chris@1040 | 191 sv_samplerate_t | 
| Chris@1122 | 192 ReadOnlyWaveFileModel::getSampleRate() const | 
| Chris@147 | 193 { | 
| Chris@147 | 194     if (!m_reader) return 0; | 
| Chris@147 | 195     return m_reader->getSampleRate(); | 
| Chris@147 | 196 } | 
| Chris@147 | 197 | 
| Chris@1040 | 198 sv_samplerate_t | 
| Chris@1122 | 199 ReadOnlyWaveFileModel::getNativeRate() const | 
| Chris@297 | 200 { | 
| Chris@297 | 201     if (!m_reader) return 0; | 
| Chris@1040 | 202     sv_samplerate_t rate = m_reader->getNativeRate(); | 
| Chris@297 | 203     if (rate == 0) rate = getSampleRate(); | 
| Chris@297 | 204     return rate; | 
| Chris@297 | 205 } | 
| Chris@297 | 206 | 
| Chris@333 | 207 QString | 
| Chris@1122 | 208 ReadOnlyWaveFileModel::getTitle() const | 
| Chris@333 | 209 { | 
| Chris@333 | 210     QString title; | 
| Chris@333 | 211     if (m_reader) title = m_reader->getTitle(); | 
| Chris@333 | 212     if (title == "") title = objectName(); | 
| Chris@333 | 213     return title; | 
| Chris@333 | 214 } | 
| Chris@333 | 215 | 
| Chris@333 | 216 QString | 
| Chris@1122 | 217 ReadOnlyWaveFileModel::getMaker() const | 
| Chris@333 | 218 { | 
| Chris@333 | 219     if (m_reader) return m_reader->getMaker(); | 
| Chris@333 | 220     return ""; | 
| Chris@333 | 221 } | 
| Chris@345 | 222 | 
| Chris@345 | 223 QString | 
| Chris@1122 | 224 ReadOnlyWaveFileModel::getLocation() const | 
| Chris@345 | 225 { | 
| Chris@345 | 226     if (m_reader) return m_reader->getLocation(); | 
| Chris@345 | 227     return ""; | 
| Chris@345 | 228 } | 
| Chris@1010 | 229 | 
| Chris@1010 | 230 QString | 
| Chris@1122 | 231 ReadOnlyWaveFileModel::getLocalFilename() const | 
| Chris@1010 | 232 { | 
| Chris@1809 | 233 #ifdef DEBUG_WAVE_FILE_MODEL | 
| Chris@1809 | 234     SVCERR << "ReadOnlyWaveFileModel::getLocalFilename: reader is " | 
| Chris@1809 | 235            << m_reader << ", returning " | 
| Chris@1809 | 236            << (m_reader ? m_reader->getLocalFilename() : "(none)") << endl; | 
| Chris@1809 | 237 #endif | 
| Chris@1010 | 238     if (m_reader) return m_reader->getLocalFilename(); | 
| Chris@1010 | 239     return ""; | 
| Chris@1010 | 240 } | 
| Chris@333 | 241 | 
| Chris@1326 | 242 floatvec_t | 
| Chris@1457 | 243 ReadOnlyWaveFileModel::getData(int channel, | 
| Chris@1457 | 244                                sv_frame_t start, | 
| Chris@1457 | 245                                sv_frame_t count) | 
| Chris@1457 | 246     const | 
| Chris@147 | 247 { | 
| Chris@1457 | 248     // Read a single channel (if channel >= 0) or a mixdown of all | 
| Chris@1457 | 249     // channels (if channel == -1) directly from the file.  This is | 
| Chris@1457 | 250     // used for e.g. audio playback or input to transforms. | 
| Chris@147 | 251 | 
| Chris@1457 | 252     Profiler profiler("ReadOnlyWaveFileModel::getData"); | 
| Chris@1457 | 253 | 
| Chris@1809 | 254 #ifdef DEBUG_WAVE_FILE_MODEL_READ | 
| Chris@1151 | 255     cout << "ReadOnlyWaveFileModel::getData[" << this << "]: " << channel << ", " << start << ", " << count << endl; | 
| Chris@429 | 256 #endif | 
| Chris@429 | 257 | 
| Chris@1100 | 258     int channels = getChannelCount(); | 
| Chris@1100 | 259 | 
| Chris@1100 | 260     if (channel >= channels) { | 
| Chris@1428 | 261         SVCERR << "ERROR: WaveFileModel::getData: channel (" | 
| Chris@1100 | 262              << channel << ") >= channel count (" << channels << ")" | 
| Chris@1100 | 263              << endl; | 
| Chris@1100 | 264         return {}; | 
| Chris@1100 | 265     } | 
| Chris@1100 | 266 | 
| Chris@1096 | 267     if (!m_reader || !m_reader->isOK() || count == 0) { | 
| Chris@1096 | 268         return {}; | 
| Chris@1096 | 269     } | 
| Chris@1096 | 270 | 
| Chris@363 | 271     if (start >= m_startFrame) { | 
| Chris@300 | 272         start -= m_startFrame; | 
| Chris@300 | 273     } else { | 
| Chris@300 | 274         if (count <= m_startFrame - start) { | 
| Chris@1100 | 275             return {}; | 
| Chris@300 | 276         } else { | 
| Chris@300 | 277             count -= (m_startFrame - start); | 
| Chris@300 | 278             start = 0; | 
| Chris@300 | 279         } | 
| Chris@147 | 280     } | 
| Chris@147 | 281 | 
| Chris@1326 | 282     floatvec_t interleaved = m_reader->getInterleavedFrames(start, count); | 
| Chris@1100 | 283     if (channels == 1) return interleaved; | 
| Chris@1100 | 284 | 
| Chris@1100 | 285     sv_frame_t obtained = interleaved.size() / channels; | 
| Chris@1100 | 286 | 
| Chris@1326 | 287     floatvec_t result(obtained, 0.f); | 
| Chris@1100 | 288 | 
| Chris@1096 | 289     if (channel != -1) { | 
| Chris@1096 | 290         // get a single channel | 
| Chris@1100 | 291         for (int i = 0; i < obtained; ++i) { | 
| Chris@1100 | 292             result[i] = interleaved[i * channels + channel]; | 
| Chris@1100 | 293         } | 
| Chris@1100 | 294     } else { | 
| Chris@1100 | 295         // channel == -1, mix down all channels | 
| Chris@1280 | 296         for (int i = 0; i < obtained; ++i) { | 
| Chris@1280 | 297             for (int c = 0; c < channels; ++c) { | 
| Chris@1100 | 298                 result[i] += interleaved[i * channels + c]; | 
| Chris@1100 | 299             } | 
| Chris@1086 | 300         } | 
| Chris@300 | 301     } | 
| Chris@147 | 302 | 
| Chris@1096 | 303     return result; | 
| Chris@147 | 304 } | 
| Chris@147 | 305 | 
| Chris@1326 | 306 vector<floatvec_t> | 
| Chris@1122 | 307 ReadOnlyWaveFileModel::getMultiChannelData(int fromchannel, int tochannel, | 
| Chris@1124 | 308                                            sv_frame_t start, sv_frame_t count) const | 
| Chris@363 | 309 { | 
| Chris@1457 | 310     // Read a set of channels directly from the file.  This is used | 
| Chris@1457 | 311     // for e.g. audio playback or input to transforms. | 
| Chris@1457 | 312 | 
| Chris@1457 | 313     Profiler profiler("ReadOnlyWaveFileModel::getMultiChannelData"); | 
| Chris@1100 | 314 | 
| Chris@1809 | 315 #ifdef DEBUG_WAVE_FILE_MODEL_READ | 
| Chris@1151 | 316     cout << "ReadOnlyWaveFileModel::getData[" << this << "]: " << fromchannel << "," << tochannel << ", " << start << ", " << count << endl; | 
| Chris@429 | 317 #endif | 
| Chris@429 | 318 | 
| Chris@929 | 319     int channels = getChannelCount(); | 
| Chris@363 | 320 | 
| Chris@363 | 321     if (fromchannel > tochannel) { | 
| Chris@1457 | 322         SVCERR << "ERROR: ReadOnlyWaveFileModel::getMultiChannelData: " | 
| Chris@1457 | 323                << "fromchannel (" << fromchannel | 
| Chris@1457 | 324                << ") > tochannel (" << tochannel << ")" | 
| Chris@1457 | 325                << endl; | 
| Chris@1096 | 326         return {}; | 
| Chris@363 | 327     } | 
| Chris@363 | 328 | 
| Chris@363 | 329     if (tochannel >= channels) { | 
| Chris@1457 | 330         SVCERR << "ERROR: ReadOnlyWaveFileModel::getMultiChannelData: " | 
| Chris@1457 | 331                << "tochannel (" << tochannel | 
| Chris@1457 | 332                << ") >= channel count (" << channels << ")" | 
| Chris@1457 | 333                << endl; | 
| Chris@1096 | 334         return {}; | 
| Chris@363 | 335     } | 
| Chris@363 | 336 | 
| Chris@1096 | 337     if (!m_reader || !m_reader->isOK() || count == 0) { | 
| Chris@1096 | 338         return {}; | 
| Chris@363 | 339     } | 
| Chris@363 | 340 | 
| Chris@929 | 341     int reqchannels = (tochannel - fromchannel) + 1; | 
| Chris@363 | 342 | 
| Chris@363 | 343     if (start >= m_startFrame) { | 
| Chris@363 | 344         start -= m_startFrame; | 
| Chris@363 | 345     } else { | 
| Chris@363 | 346         if (count <= m_startFrame - start) { | 
| Chris@1096 | 347             return {}; | 
| Chris@363 | 348         } else { | 
| Chris@363 | 349             count -= (m_startFrame - start); | 
| Chris@363 | 350             start = 0; | 
| Chris@363 | 351         } | 
| Chris@363 | 352     } | 
| Chris@363 | 353 | 
| Chris@1326 | 354     floatvec_t interleaved = m_reader->getInterleavedFrames(start, count); | 
| Chris@1096 | 355     if (channels == 1) return { interleaved }; | 
| Chris@1096 | 356 | 
| Chris@1096 | 357     sv_frame_t obtained = interleaved.size() / channels; | 
| Chris@1326 | 358     vector<floatvec_t> result(reqchannels, floatvec_t(obtained, 0.f)); | 
| Chris@1096 | 359 | 
| Chris@1096 | 360     for (int c = fromchannel; c <= tochannel; ++c) { | 
| Chris@1096 | 361         int destc = c - fromchannel; | 
| Chris@1096 | 362         for (int i = 0; i < obtained; ++i) { | 
| Chris@1096 | 363             result[destc][i] = interleaved[i * channels + c]; | 
| Chris@363 | 364         } | 
| Chris@363 | 365     } | 
| Chris@1096 | 366 | 
| Chris@1096 | 367     return result; | 
| Chris@363 | 368 } | 
| Chris@363 | 369 | 
| Chris@929 | 370 int | 
| Chris@1122 | 371 ReadOnlyWaveFileModel::getSummaryBlockSize(int desired) const | 
| Chris@377 | 372 { | 
| Chris@377 | 373     int cacheType = 0; | 
| Chris@377 | 374     int power = m_zoomConstraint.getMinCachePower(); | 
| Chris@929 | 375     int roundedBlockSize = m_zoomConstraint.getNearestBlockSize | 
| Chris@377 | 376         (desired, cacheType, power, ZoomConstraint::RoundDown); | 
| Chris@1324 | 377 | 
| Chris@377 | 378     if (cacheType != 0 && cacheType != 1) { | 
| Chris@377 | 379         // We will be reading directly from file, so can satisfy any | 
| Chris@377 | 380         // blocksize requirement | 
| Chris@377 | 381         return desired; | 
| Chris@377 | 382     } else { | 
| Chris@377 | 383         return roundedBlockSize; | 
| Chris@377 | 384     } | 
| Chris@377 | 385 } | 
| Chris@377 | 386 | 
| Chris@225 | 387 void | 
| Chris@1122 | 388 ReadOnlyWaveFileModel::getSummaries(int channel, sv_frame_t start, sv_frame_t count, | 
| Chris@1151 | 389                                     RangeBlock &ranges, int &blockSize) const | 
| Chris@147 | 390 { | 
| Chris@225 | 391     ranges.clear(); | 
| Chris@225 | 392     if (!isOK()) return; | 
| Chris@377 | 393     ranges.reserve((count / blockSize) + 1); | 
| Chris@147 | 394 | 
| Chris@300 | 395     if (start > m_startFrame) start -= m_startFrame; | 
| Chris@300 | 396     else if (count <= m_startFrame - start) return; | 
| Chris@300 | 397     else { | 
| Chris@300 | 398         count -= (m_startFrame - start); | 
| Chris@300 | 399         start = 0; | 
| Chris@147 | 400     } | 
| Chris@147 | 401 | 
| Chris@147 | 402     int cacheType = 0; | 
| Chris@179 | 403     int power = m_zoomConstraint.getMinCachePower(); | 
| Chris@929 | 404     int roundedBlockSize = m_zoomConstraint.getNearestBlockSize | 
| Chris@377 | 405         (blockSize, cacheType, power, ZoomConstraint::RoundDown); | 
| Chris@147 | 406 | 
| Chris@929 | 407     int channels = getChannelCount(); | 
| Chris@147 | 408 | 
| Chris@147 | 409     if (cacheType != 0 && cacheType != 1) { | 
| Chris@147 | 410 | 
| Chris@1406 | 411         // We need to read directly from the file.  We haven't got | 
| Chris@1406 | 412         // this cached.  Hope the requested area is small.  This is | 
| Chris@1406 | 413         // not optimal -- we'll end up reading the same frames twice | 
| Chris@1406 | 414         // for stereo files, in two separate calls to this method. | 
| Chris@1406 | 415         // We could fairly trivially handle this for most cases that | 
| Chris@1406 | 416         // matter by putting a single cache in getInterleavedFrames | 
| Chris@1406 | 417         // for short queries. | 
| Chris@147 | 418 | 
| Chris@377 | 419         m_directReadMutex.lock(); | 
| Chris@377 | 420 | 
| Chris@377 | 421         if (m_lastDirectReadStart != start || | 
| Chris@377 | 422             m_lastDirectReadCount != count || | 
| Chris@377 | 423             m_directRead.empty()) { | 
| Chris@377 | 424 | 
| Chris@1041 | 425             m_directRead = m_reader->getInterleavedFrames(start, count); | 
| Chris@377 | 426             m_lastDirectReadStart = start; | 
| Chris@377 | 427             m_lastDirectReadCount = count; | 
| Chris@377 | 428         } | 
| Chris@377 | 429 | 
| Chris@1406 | 430         float max = 0.0, min = 0.0, total = 0.0; | 
| Chris@1406 | 431         sv_frame_t i = 0, got = 0; | 
| Chris@147 | 432 | 
| Chris@1406 | 433         while (i < count) { | 
| Chris@147 | 434 | 
| Chris@1406 | 435             sv_frame_t index = i * channels + channel; | 
| Chris@1406 | 436             if (index >= (sv_frame_t)m_directRead.size()) break; | 
| Chris@147 | 437 | 
| Chris@1406 | 438             float sample = m_directRead[index]; | 
| Chris@300 | 439             if (sample > max || got == 0) max = sample; | 
| Chris@1406 | 440             if (sample < min || got == 0) min = sample; | 
| Chris@147 | 441             total += fabsf(sample); | 
| Chris@838 | 442 | 
| Chris@1406 | 443             ++i; | 
| Chris@300 | 444             ++got; | 
| Chris@147 | 445 | 
| Chris@300 | 446             if (got == blockSize) { | 
| Chris@1038 | 447                 ranges.push_back(Range(min, max, total / float(got))); | 
| Chris@147 | 448                 min = max = total = 0.0f; | 
| Chris@300 | 449                 got = 0; | 
| Chris@1406 | 450             } | 
| Chris@1406 | 451         } | 
| Chris@147 | 452 | 
| Chris@377 | 453         m_directReadMutex.unlock(); | 
| Chris@377 | 454 | 
| Chris@1406 | 455         if (got > 0) { | 
| Chris@1038 | 456             ranges.push_back(Range(min, max, total / float(got))); | 
| Chris@1406 | 457         } | 
| Chris@147 | 458 | 
| Chris@1406 | 459         return; | 
| Chris@147 | 460 | 
| Chris@147 | 461     } else { | 
| Chris@147 | 462 | 
| Chris@1406 | 463         QMutexLocker locker(&m_mutex); | 
| Chris@147 | 464 | 
| Chris@1406 | 465         const RangeBlock &cache = m_cache[cacheType]; | 
| Chris@147 | 466 | 
| Chris@377 | 467         blockSize = roundedBlockSize; | 
| Chris@377 | 468 | 
| Chris@1406 | 469         sv_frame_t cacheBlock, div; | 
| Chris@1152 | 470 | 
| Chris@1152 | 471         cacheBlock = (sv_frame_t(1) << m_zoomConstraint.getMinCachePower()); | 
| Chris@1406 | 472         if (cacheType == 1) { | 
| Chris@1406 | 473             cacheBlock = sv_frame_t(double(cacheBlock) * sqrt(2.) + 0.01); | 
| Chris@1406 | 474         } | 
| Chris@1152 | 475         div = blockSize / cacheBlock; | 
| Chris@147 | 476 | 
| Chris@1406 | 477         sv_frame_t startIndex = start / cacheBlock; | 
| Chris@1406 | 478         sv_frame_t endIndex = (start + count) / cacheBlock; | 
| Chris@147 | 479 | 
| Chris@1406 | 480         float max = 0.0, min = 0.0, total = 0.0; | 
| Chris@1406 | 481         sv_frame_t i = 0, got = 0; | 
| Chris@147 | 482 | 
| Chris@1809 | 483 #ifdef DEBUG_WAVE_FILE_MODEL_READ | 
| Chris@1406 | 484         cerr << "blockSize is " << blockSize << ", cacheBlock " << cacheBlock << ", start " << start << ", count " << count << " (frame count " << getFrameCount() << "), power is " << power << ", div is " << div << ", startIndex " << startIndex << ", endIndex " << endIndex << endl; | 
| Chris@236 | 485 #endif | 
| Chris@147 | 486 | 
| Chris@1406 | 487         for (i = 0; i <= endIndex - startIndex; ) { | 
| Chris@147 | 488 | 
| Chris@1406 | 489             sv_frame_t index = (i + startIndex) * channels + channel; | 
| Chris@1406 | 490             if (!in_range_for(cache, index)) break; | 
| Chris@147 | 491 | 
| Chris@147 | 492             const Range &range = cache[index]; | 
| Chris@410 | 493             if (range.max() > max || got == 0) max = range.max(); | 
| Chris@410 | 494             if (range.min() < min || got == 0) min = range.min(); | 
| Chris@410 | 495             total += range.absmean(); | 
| Chris@147 | 496 | 
| Chris@1406 | 497             ++i; | 
| Chris@300 | 498             ++got; | 
| Chris@147 | 499 | 
| Chris@1406 | 500             if (got == div) { | 
| Chris@1406 | 501                 ranges.push_back(Range(min, max, total / float(got))); | 
| Chris@147 | 502                 min = max = total = 0.0f; | 
| Chris@300 | 503                 got = 0; | 
| Chris@1406 | 504             } | 
| Chris@1406 | 505         } | 
| Chris@1406 | 506 | 
| Chris@1406 | 507         if (got > 0) { | 
| Chris@1038 | 508             ranges.push_back(Range(min, max, total / float(got))); | 
| Chris@1406 | 509         } | 
| Chris@147 | 510     } | 
| Chris@147 | 511 | 
| Chris@1809 | 512 #ifdef DEBUG_WAVE_FILE_MODEL_READ | 
| Chris@1151 | 513     cerr << "returning " << ranges.size() << " ranges" << endl; | 
| Chris@236 | 514 #endif | 
| Chris@225 | 515     return; | 
| Chris@147 | 516 } | 
| Chris@147 | 517 | 
| Chris@1122 | 518 ReadOnlyWaveFileModel::Range | 
| Chris@1122 | 519 ReadOnlyWaveFileModel::getSummary(int channel, sv_frame_t start, sv_frame_t count) const | 
| Chris@147 | 520 { | 
| Chris@147 | 521     Range range; | 
| Chris@147 | 522     if (!isOK()) return range; | 
| Chris@147 | 523 | 
| Chris@300 | 524     if (start > m_startFrame) start -= m_startFrame; | 
| Chris@300 | 525     else if (count <= m_startFrame - start) return range; | 
| Chris@300 | 526     else { | 
| Chris@300 | 527         count -= (m_startFrame - start); | 
| Chris@300 | 528         start = 0; | 
| Chris@147 | 529     } | 
| Chris@147 | 530 | 
| Chris@929 | 531     int blockSize; | 
| Chris@300 | 532     for (blockSize = 1; blockSize <= count; blockSize *= 2); | 
| Chris@300 | 533     if (blockSize > 1) blockSize /= 2; | 
| Chris@147 | 534 | 
| Chris@147 | 535     bool first = false; | 
| Chris@147 | 536 | 
| Chris@1038 | 537     sv_frame_t blockStart = (start / blockSize) * blockSize; | 
| Chris@1038 | 538     sv_frame_t blockEnd = ((start + count) / blockSize) * blockSize; | 
| Chris@147 | 539 | 
| Chris@147 | 540     if (blockStart < start) blockStart += blockSize; | 
| Chris@147 | 541 | 
| Chris@147 | 542     if (blockEnd > blockStart) { | 
| Chris@225 | 543         RangeBlock ranges; | 
| Chris@300 | 544         getSummaries(channel, blockStart, blockEnd - blockStart, ranges, blockSize); | 
| Chris@929 | 545         for (int i = 0; i < (int)ranges.size(); ++i) { | 
| Chris@410 | 546             if (first || ranges[i].min() < range.min()) range.setMin(ranges[i].min()); | 
| Chris@410 | 547             if (first || ranges[i].max() > range.max()) range.setMax(ranges[i].max()); | 
| Chris@410 | 548             if (first || ranges[i].absmean() < range.absmean()) range.setAbsmean(ranges[i].absmean()); | 
| Chris@147 | 549             first = false; | 
| Chris@147 | 550         } | 
| Chris@147 | 551     } | 
| Chris@147 | 552 | 
| Chris@147 | 553     if (blockStart > start) { | 
| Chris@300 | 554         Range startRange = getSummary(channel, start, blockStart - start); | 
| Chris@1096 | 555         range.setMin(min(range.min(), startRange.min())); | 
| Chris@1096 | 556         range.setMax(max(range.max(), startRange.max())); | 
| Chris@1096 | 557         range.setAbsmean(min(range.absmean(), startRange.absmean())); | 
| Chris@147 | 558     } | 
| Chris@147 | 559 | 
| Chris@300 | 560     if (blockEnd < start + count) { | 
| Chris@300 | 561         Range endRange = getSummary(channel, blockEnd, start + count - blockEnd); | 
| Chris@1096 | 562         range.setMin(min(range.min(), endRange.min())); | 
| Chris@1096 | 563         range.setMax(max(range.max(), endRange.max())); | 
| Chris@1096 | 564         range.setAbsmean(min(range.absmean(), endRange.absmean())); | 
| Chris@147 | 565     } | 
| Chris@147 | 566 | 
| Chris@147 | 567     return range; | 
| Chris@147 | 568 } | 
| Chris@147 | 569 | 
| Chris@147 | 570 void | 
| Chris@1122 | 571 ReadOnlyWaveFileModel::fillCache() | 
| Chris@147 | 572 { | 
| Chris@147 | 573     m_mutex.lock(); | 
| Chris@188 | 574 | 
| Chris@147 | 575     m_updateTimer = new QTimer(this); | 
| Chris@147 | 576     connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(fillTimerTimedOut())); | 
| Chris@147 | 577     m_updateTimer->start(100); | 
| Chris@188 | 578 | 
| Chris@147 | 579     m_fillThread = new RangeCacheFillThread(*this); | 
| Chris@147 | 580     connect(m_fillThread, SIGNAL(finished()), this, SLOT(cacheFilled())); | 
| Chris@188 | 581 | 
| Chris@147 | 582     m_mutex.unlock(); | 
| Chris@147 | 583     m_fillThread->start(); | 
| Chris@188 | 584 | 
| Chris@236 | 585 #ifdef DEBUG_WAVE_FILE_MODEL | 
| Chris@1809 | 586     SVCERR << "ReadOnlyWaveFileModel(" << objectName() << ")::fillCache: started fill thread" << endl; | 
| Chris@236 | 587 #endif | 
| Chris@147 | 588 } | 
| Chris@147 | 589 | 
| Chris@147 | 590 void | 
| Chris@1122 | 591 ReadOnlyWaveFileModel::fillTimerTimedOut() | 
| Chris@147 | 592 { | 
| Chris@147 | 593     if (m_fillThread) { | 
| Chris@1406 | 594         sv_frame_t fillExtent = m_fillThread->getFillExtent(); | 
| Chris@236 | 595 #ifdef DEBUG_WAVE_FILE_MODEL | 
| Chris@1809 | 596         SVCERR << "ReadOnlyWaveFileModel(" << objectName() << ")::fillTimerTimedOut: extent = " << fillExtent << endl; | 
| Chris@236 | 597 #endif | 
| Chris@1406 | 598         if (fillExtent > m_lastFillExtent) { | 
| Chris@1752 | 599             emit modelChangedWithin(getId(), m_lastFillExtent, fillExtent); | 
| Chris@1406 | 600             m_lastFillExtent = fillExtent; | 
| Chris@1406 | 601         } | 
| Chris@147 | 602     } else { | 
| Chris@236 | 603 #ifdef DEBUG_WAVE_FILE_MODEL | 
| Chris@1809 | 604         SVCERR << "ReadOnlyWaveFileModel(" << objectName() << ")::fillTimerTimedOut: no thread" << endl; | 
| Chris@236 | 605 #endif | 
| Chris@1752 | 606         emit modelChanged(getId()); | 
| Chris@147 | 607     } | 
| Chris@147 | 608 } | 
| Chris@147 | 609 | 
| Chris@147 | 610 void | 
| Chris@1122 | 611 ReadOnlyWaveFileModel::cacheFilled() | 
| Chris@147 | 612 { | 
| Chris@147 | 613     m_mutex.lock(); | 
| Chris@147 | 614     delete m_fillThread; | 
| Chris@1582 | 615     m_fillThread = nullptr; | 
| Chris@147 | 616     delete m_updateTimer; | 
| Chris@1582 | 617     m_updateTimer = nullptr; | 
| Chris@1557 | 618     auto prevFillExtent = m_lastFillExtent; | 
| Chris@1557 | 619     m_lastFillExtent = getEndFrame(); | 
| Chris@147 | 620     m_mutex.unlock(); | 
| Chris@1557 | 621 #ifdef DEBUG_WAVE_FILE_MODEL | 
| Chris@1809 | 622     SVCERR << "ReadOnlyWaveFileModel(" << objectName() << ")::cacheFilled, about to emit things" << endl; | 
| Chris@1557 | 623 #endif | 
| Chris@1557 | 624     if (getEndFrame() > prevFillExtent) { | 
| Chris@1752 | 625         emit modelChangedWithin(getId(), prevFillExtent, getEndFrame()); | 
| Chris@267 | 626     } | 
| Chris@1752 | 627     emit modelChanged(getId()); | 
| Chris@1752 | 628     emit ready(getId()); | 
| Chris@175 | 629 } | 
| Chris@175 | 630 | 
| Chris@175 | 631 void | 
| Chris@1122 | 632 ReadOnlyWaveFileModel::RangeCacheFillThread::run() | 
| Chris@147 | 633 { | 
| Chris@929 | 634     int cacheBlockSize[2]; | 
| Chris@179 | 635     cacheBlockSize[0] = (1 << m_model.m_zoomConstraint.getMinCachePower()); | 
| Chris@1038 | 636     cacheBlockSize[1] = (int((1 << m_model.m_zoomConstraint.getMinCachePower()) * | 
| Chris@608 | 637                                         sqrt(2.) + 0.01)); | 
| Chris@147 | 638 | 
| Chris@1038 | 639     sv_frame_t frame = 0; | 
| Chris@1151 | 640     const sv_frame_t readBlockSize = 32768; | 
| Chris@1326 | 641     floatvec_t block; | 
| Chris@147 | 642 | 
| Chris@147 | 643     if (!m_model.isOK()) return; | 
| Chris@147 | 644 | 
| Chris@929 | 645     int channels = m_model.getChannelCount(); | 
| Chris@187 | 646     bool updating = m_model.m_reader->isUpdating(); | 
| Chris@187 | 647 | 
| Chris@187 | 648     if (updating) { | 
| Chris@187 | 649         while (channels == 0 && !m_model.m_exiting) { | 
| Chris@1151 | 650 #ifdef DEBUG_WAVE_FILE_MODEL | 
| Chris@1809 | 651             SVCERR << "ReadOnlyWaveFileModel(" << objectName() << ")::fill: Waiting for channels..." << endl; | 
| Chris@1151 | 652 #endif | 
| Chris@187 | 653             sleep(1); | 
| Chris@187 | 654             channels = m_model.getChannelCount(); | 
| Chris@187 | 655         } | 
| Chris@187 | 656     } | 
| Chris@147 | 657 | 
| Chris@147 | 658     Range *range = new Range[2 * channels]; | 
| Chris@410 | 659     float *means = new float[2 * channels]; | 
| Chris@929 | 660     int count[2]; | 
| Chris@147 | 661     count[0] = count[1] = 0; | 
| Chris@411 | 662     for (int i = 0; i < 2 * channels; ++i) { | 
| Chris@411 | 663         means[i] = 0.f; | 
| Chris@411 | 664     } | 
| Chris@176 | 665 | 
| Chris@176 | 666     bool first = true; | 
| Chris@176 | 667 | 
| Chris@176 | 668     while (first || updating) { | 
| Chris@176 | 669 | 
| Chris@176 | 670         updating = m_model.m_reader->isUpdating(); | 
| Chris@187 | 671         m_frameCount = m_model.getFrameCount(); | 
| Chris@175 | 672 | 
| Chris@1151 | 673         m_model.m_mutex.lock(); | 
| Chris@147 | 674 | 
| Chris@176 | 675         while (frame < m_frameCount) { | 
| Chris@147 | 676 | 
| Chris@1151 | 677             m_model.m_mutex.unlock(); | 
| Chris@1151 | 678 | 
| Chris@1809 | 679 #ifdef DEBUG_WAVE_FILE_MODEL_READ | 
| Chris@1809 | 680             cout << "ReadOnlyWaveFileModel(" << m_model.objectName() << ")::fill inner loop: frame = " << frame << ", count = " << m_frameCount << ", blocksize " << readBlockSize << endl; | 
| Chris@1151 | 681 #endif | 
| Chris@265 | 682 | 
| Chris@1220 | 683             if (updating && (frame + readBlockSize > m_frameCount)) { | 
| Chris@1220 | 684                 m_model.m_mutex.lock(); // must be locked on exiting loop | 
| Chris@1220 | 685                 break; | 
| Chris@1220 | 686             } | 
| Chris@176 | 687 | 
| Chris@1041 | 688             block = m_model.m_reader->getInterleavedFrames(frame, readBlockSize); | 
| Chris@176 | 689 | 
| Chris@1151 | 690             sv_frame_t gotBlockSize = block.size() / channels; | 
| Chris@265 | 691 | 
| Chris@1151 | 692             m_model.m_mutex.lock(); | 
| Chris@1151 | 693 | 
| Chris@1151 | 694             for (sv_frame_t i = 0; i < gotBlockSize; ++i) { | 
| Chris@1406 | 695 | 
| Chris@411 | 696                 for (int ch = 0; ch < channels; ++ch) { | 
| Chris@147 | 697 | 
| Chris@1038 | 698                     sv_frame_t index = channels * i + ch; | 
| Chris@176 | 699                     float sample = block[index]; | 
| Chris@176 | 700 | 
| Chris@1151 | 701                     for (int cacheType = 0; cacheType < 2; ++cacheType) { | 
| Chris@1053 | 702                         sv_frame_t rangeIndex = ch * 2 + cacheType; | 
| Chris@1053 | 703                         range[rangeIndex].sample(sample); | 
| Chris@410 | 704                         means[rangeIndex] += fabsf(sample); | 
| Chris@176 | 705                     } | 
| Chris@176 | 706                 } | 
| Chris@1042 | 707 | 
| Chris@1053 | 708                 for (int cacheType = 0; cacheType < 2; ++cacheType) { | 
| Chris@232 | 709 | 
| Chris@1053 | 710                     if (++count[cacheType] == cacheBlockSize[cacheType]) { | 
| Chris@410 | 711 | 
| Chris@929 | 712                         for (int ch = 0; ch < int(channels); ++ch) { | 
| Chris@1053 | 713                             int rangeIndex = ch * 2 + cacheType; | 
| Chris@1053 | 714                             means[rangeIndex] = means[rangeIndex] / float(count[cacheType]); | 
| Chris@410 | 715                             range[rangeIndex].setAbsmean(means[rangeIndex]); | 
| Chris@1053 | 716                             m_model.m_cache[cacheType].push_back(range[rangeIndex]); | 
| Chris@176 | 717                             range[rangeIndex] = Range(); | 
| Chris@411 | 718                             means[rangeIndex] = 0.f; | 
| Chris@176 | 719                         } | 
| Chris@232 | 720 | 
| Chris@1053 | 721                         count[cacheType] = 0; | 
| Chris@176 | 722                     } | 
| Chris@176 | 723                 } | 
| Chris@147 | 724 | 
| Chris@176 | 725                 ++frame; | 
| Chris@147 | 726             } | 
| Chris@1151 | 727 | 
| Chris@176 | 728             if (m_model.m_exiting) break; | 
| Chris@176 | 729             m_fillExtent = frame; | 
| Chris@147 | 730         } | 
| Chris@147 | 731 | 
| Chris@1151 | 732         m_model.m_mutex.unlock(); | 
| Chris@1151 | 733 | 
| Chris@176 | 734         first = false; | 
| Chris@177 | 735         if (m_model.m_exiting) break; | 
| Chris@187 | 736         if (updating) { | 
| Chris@187 | 737             sleep(1); | 
| Chris@187 | 738         } | 
| Chris@147 | 739     } | 
| Chris@147 | 740 | 
| Chris@177 | 741     if (!m_model.m_exiting) { | 
| Chris@177 | 742 | 
| Chris@177 | 743         QMutexLocker locker(&m_model.m_mutex); | 
| Chris@232 | 744 | 
| Chris@1053 | 745         for (int cacheType = 0; cacheType < 2; ++cacheType) { | 
| Chris@232 | 746 | 
| Chris@1053 | 747             if (count[cacheType] > 0) { | 
| Chris@232 | 748 | 
| Chris@929 | 749                 for (int ch = 0; ch < int(channels); ++ch) { | 
| Chris@1053 | 750                     int rangeIndex = ch * 2 + cacheType; | 
| Chris@1053 | 751                     means[rangeIndex] = means[rangeIndex] / float(count[cacheType]); | 
| Chris@410 | 752                     range[rangeIndex].setAbsmean(means[rangeIndex]); | 
| Chris@1053 | 753                     m_model.m_cache[cacheType].push_back(range[rangeIndex]); | 
| Chris@177 | 754                     range[rangeIndex] = Range(); | 
| Chris@411 | 755                     means[rangeIndex] = 0.f; | 
| Chris@177 | 756                 } | 
| Chris@232 | 757 | 
| Chris@1053 | 758                 count[cacheType] = 0; | 
| Chris@147 | 759             } | 
| Chris@177 | 760 | 
| Chris@1053 | 761             const Range &rr = *m_model.m_cache[cacheType].begin(); | 
| Chris@1053 | 762             MUNLOCK(&rr, m_model.m_cache[cacheType].capacity() * sizeof(Range)); | 
| Chris@147 | 763         } | 
| Chris@147 | 764     } | 
| Chris@147 | 765 | 
| Chris@410 | 766     delete[] means; | 
| Chris@147 | 767     delete[] range; | 
| Chris@147 | 768 | 
| Chris@175 | 769     m_fillExtent = m_frameCount; | 
| Chris@236 | 770 | 
| Chris@236 | 771 #ifdef DEBUG_WAVE_FILE_MODEL | 
| Chris@1053 | 772     for (int cacheType = 0; cacheType < 2; ++cacheType) { | 
| Chris@1809 | 773         SVCERR << "ReadOnlyWaveFileModel(" << m_model.objectName() << "): Cache type " << cacheType << " now contains " << m_model.m_cache[cacheType].size() << " ranges" << endl; | 
| Chris@236 | 774     } | 
| Chris@236 | 775 #endif | 
| Chris@147 | 776 } | 
| Chris@147 | 777 | 
| Chris@163 | 778 void | 
| Chris@1122 | 779 ReadOnlyWaveFileModel::toXml(QTextStream &out, | 
| Chris@163 | 780                      QString indent, | 
| Chris@163 | 781                      QString extraAttributes) const | 
| Chris@163 | 782 { | 
| Chris@163 | 783     Model::toXml(out, indent, | 
| Chris@163 | 784                  QString("type=\"wavefile\" file=\"%1\" %2") | 
| Chris@279 | 785                  .arg(encodeEntities(m_path)).arg(extraAttributes)); | 
| Chris@163 | 786 } | 
| Chris@163 | 787 | 
| Chris@147 | 788 |