comparison data/model/ReadOnlyWaveFileModel.cpp @ 1365:3382d914e110

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