annotate data/fileio/WavFileReader.cpp @ 1700:c1208b211d8c single-point

Ensure test fails rather than crashing if this reader doesn't get created
author Chris Cannam <cannam@all-day-breakfast.com>
date Fri, 03 May 2019 15:02:09 +0100
parents ce185d4dd408
children 14747f24ad04
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@202 7 This file copyright 2006 Chris Cannam and QMUL.
Chris@148 8
Chris@148 9 This program is free software; you can redistribute it and/or
Chris@148 10 modify it under the terms of the GNU General Public License as
Chris@148 11 published by the Free Software Foundation; either version 2 of the
Chris@148 12 License, or (at your option) any later version. See the file
Chris@148 13 COPYING included with this distribution for more information.
Chris@148 14 */
Chris@148 15
Chris@148 16 #include "WavFileReader.h"
Chris@148 17
Chris@1256 18 #include "base/HitCount.h"
Chris@1258 19 #include "base/Profiler.h"
Chris@1256 20
Chris@148 21 #include <iostream>
Chris@148 22
Chris@175 23 #include <QMutexLocker>
Chris@316 24 #include <QFileInfo>
Chris@175 25
Chris@1096 26 using namespace std;
Chris@1096 27
Chris@1513 28 WavFileReader::WavFileReader(FileSource source,
Chris@1513 29 bool fileUpdating,
Chris@1520 30 Normalisation normalisation) :
Chris@1582 31 m_file(nullptr),
Chris@316 32 m_source(source),
Chris@316 33 m_path(source.getLocalFilename()),
Chris@823 34 m_seekable(false),
Chris@148 35 m_lastStart(0),
Chris@176 36 m_lastCount(0),
Chris@1520 37 m_normalisation(normalisation),
Chris@1513 38 m_max(0.f),
Chris@176 39 m_updating(fileUpdating)
Chris@148 40 {
Chris@148 41 m_frameCount = 0;
Chris@148 42 m_channelCount = 0;
Chris@148 43 m_sampleRate = 0;
Chris@148 44
Chris@148 45 m_fileInfo.format = 0;
Chris@148 46 m_fileInfo.frames = 0;
Chris@148 47
Chris@1349 48 #ifdef Q_OS_WIN
Chris@1349 49 m_file = sf_wchar_open((LPCWSTR)m_path.utf16(), SFM_READ, &m_fileInfo);
Chris@1349 50 #else
Chris@1349 51 m_file = sf_open(m_path.toLocal8Bit(), SFM_READ, &m_fileInfo);
Chris@1349 52 #endif
Chris@1349 53
Chris@1349 54 if (!m_file || (!fileUpdating && m_fileInfo.channels <= 0)) {
Chris@1343 55 SVDEBUG << "WavFileReader::initialize: Failed to open file at \""
Chris@1349 56 << m_path << "\" ("
Chris@1349 57 << sf_strerror(m_file) << ")" << endl;
Chris@148 58
Chris@1349 59 if (m_file) {
Chris@1343 60 m_error = QString("Couldn't load audio file '%1':\n%2")
Chris@1349 61 .arg(m_path).arg(sf_strerror(m_file));
Chris@1343 62 } else {
Chris@1349 63 m_error = QString("Failed to open audio file '%1'")
Chris@1349 64 .arg(m_path);
Chris@1343 65 }
Chris@1343 66 return;
Chris@148 67 }
Chris@148 68
Chris@187 69 if (m_fileInfo.channels > 0) {
Chris@823 70
Chris@187 71 m_frameCount = m_fileInfo.frames;
Chris@187 72 m_channelCount = m_fileInfo.channels;
Chris@187 73 m_sampleRate = m_fileInfo.samplerate;
Chris@823 74
Chris@823 75 m_seekable = (m_fileInfo.seekable != 0);
Chris@823 76
Chris@823 77 int type = m_fileInfo.format & SF_FORMAT_TYPEMASK;
Chris@1162 78 int subtype = m_fileInfo.format & SF_FORMAT_SUBMASK;
Chris@1162 79
Chris@823 80 if (type >= SF_FORMAT_FLAC || type >= SF_FORMAT_OGG) {
Chris@1162 81 // Our m_seekable reports whether a file is rapidly
Chris@1162 82 // seekable, so things like Ogg don't qualify. We
Chris@1162 83 // cautiously report every file type of "at least" the
Chris@1162 84 // historical period of Ogg or FLAC as non-seekable.
Chris@823 85 m_seekable = false;
Chris@1162 86 } else if (type == SF_FORMAT_WAV && subtype <= SF_FORMAT_DOUBLE) {
Chris@1162 87 // libsndfile 1.0.26 has a bug (subsequently fixed in the
Chris@1162 88 // repo) that causes all files to be reported as
Chris@1162 89 // non-seekable. We know that certain common file types
Chris@1162 90 // are definitely seekable so, again cautiously, identify
Chris@1162 91 // and mark those (basically only non-adaptive WAVs).
Chris@1162 92 m_seekable = true;
Chris@823 93 }
Chris@1513 94
Chris@1520 95 if (m_normalisation != Normalisation::None && !m_updating) {
Chris@1513 96 m_max = getMax();
Chris@1513 97 }
Chris@1592 98
Chris@1592 99 const char *str = sf_get_string(m_file, SF_STR_TITLE);
Chris@1592 100 if (str) {
Chris@1592 101 m_title = str;
Chris@1592 102 }
Chris@1592 103 str = sf_get_string(m_file, SF_STR_ARTIST);
Chris@1592 104 if (str) {
Chris@1592 105 m_maker = str;
Chris@1592 106 }
Chris@187 107 }
Chris@175 108
Chris@1520 109 SVDEBUG << "WavFileReader: Filename " << m_path << ", frame count " << m_frameCount << ", channel count " << m_channelCount << ", sample rate " << m_sampleRate << ", format " << m_fileInfo.format << ", seekable " << m_fileInfo.seekable << " adjusted to " << m_seekable << ", normalisation " << int(m_normalisation) << endl;
Chris@148 110 }
Chris@148 111
Chris@148 112 WavFileReader::~WavFileReader()
Chris@148 113 {
Chris@1349 114 if (m_file) sf_close(m_file);
Chris@148 115 }
Chris@148 116
Chris@148 117 void
Chris@175 118 WavFileReader::updateFrameCount()
Chris@175 119 {
Chris@175 120 QMutexLocker locker(&m_mutex);
Chris@175 121
Chris@1038 122 sv_frame_t prevCount = m_fileInfo.frames;
Chris@175 123
Chris@1349 124 if (m_file) {
Chris@1349 125 sf_close(m_file);
Chris@1349 126 #ifdef Q_OS_WIN
Chris@1349 127 m_file = sf_wchar_open((LPCWSTR)m_path.utf16(), SFM_READ, &m_fileInfo);
Chris@1349 128 #else
Chris@1349 129 m_file = sf_open(m_path.toLocal8Bit(), SFM_READ, &m_fileInfo);
Chris@1349 130 #endif
Chris@1349 131 if (!m_file || m_fileInfo.channels <= 0) {
Chris@1349 132 SVDEBUG << "WavFileReader::updateFrameCount: Failed to open file at \"" << m_path << "\" ("
Chris@1349 133 << sf_strerror(m_file) << ")" << endl;
Chris@175 134 }
Chris@175 135 }
Chris@175 136
Chris@690 137 // SVDEBUG << "WavFileReader::updateFrameCount: now " << m_fileInfo.frames << endl;
Chris@175 138
Chris@176 139 m_frameCount = m_fileInfo.frames;
Chris@176 140
Chris@187 141 if (m_channelCount == 0) {
Chris@187 142 m_channelCount = m_fileInfo.channels;
Chris@187 143 m_sampleRate = m_fileInfo.samplerate;
Chris@187 144 }
Chris@187 145
Chris@258 146 if (m_frameCount != prevCount) {
Chris@258 147 emit frameCountChanged();
Chris@258 148 }
Chris@176 149 }
Chris@176 150
Chris@176 151 void
Chris@176 152 WavFileReader::updateDone()
Chris@176 153 {
Chris@176 154 updateFrameCount();
Chris@176 155 m_updating = false;
Chris@1520 156 if (m_normalisation != Normalisation::None) {
Chris@1513 157 m_max = getMax();
Chris@1513 158 }
Chris@175 159 }
Chris@175 160
Chris@1326 161 floatvec_t
Chris@1041 162 WavFileReader::getInterleavedFrames(sv_frame_t start, sv_frame_t count) const
Chris@148 163 {
Chris@1513 164 floatvec_t frames = getInterleavedFramesUnnormalised(start, count);
Chris@1513 165
Chris@1520 166 if (m_normalisation == Normalisation::None || m_max == 0.f) {
Chris@1513 167 return frames;
Chris@1513 168 }
Chris@1513 169
Chris@1513 170 for (int i = 0; in_range_for(frames, i); ++i) {
Chris@1513 171 frames[i] /= m_max;
Chris@1513 172 }
Chris@1513 173
Chris@1513 174 return frames;
Chris@1513 175 }
Chris@1513 176
Chris@1513 177 floatvec_t
Chris@1513 178 WavFileReader::getInterleavedFramesUnnormalised(sv_frame_t start,
Chris@1513 179 sv_frame_t count) const
Chris@1513 180 {
Chris@1256 181 static HitCount lastRead("WavFileReader: last read");
Chris@1256 182
Chris@1096 183 if (count == 0) return {};
Chris@175 184
Chris@175 185 QMutexLocker locker(&m_mutex);
Chris@175 186
Chris@1258 187 Profiler profiler("WavFileReader::getInterleavedFrames");
Chris@1258 188
Chris@1349 189 if (!m_file || !m_channelCount) {
Chris@1096 190 return {};
Chris@175 191 }
Chris@148 192
Chris@1040 193 if (start >= m_fileInfo.frames) {
Chris@690 194 // SVDEBUG << "WavFileReader::getInterleavedFrames: " << start
Chris@687 195 // << " > " << m_fileInfo.frames << endl;
Chris@1343 196 return {};
Chris@148 197 }
Chris@148 198
Chris@1040 199 if (start + count > m_fileInfo.frames) {
Chris@1343 200 count = m_fileInfo.frames - start;
Chris@148 201 }
Chris@148 202
Chris@1096 203 // Because WaveFileModel::getSummaries() is called separately for
Chris@1096 204 // individual channels, it's quite common for us to be called
Chris@1096 205 // repeatedly for the same data. So this is worth cacheing.
Chris@1096 206 if (start == m_lastStart && count == m_lastCount) {
Chris@1256 207 lastRead.hit();
Chris@1096 208 return m_buffer;
Chris@1096 209 }
Chris@1256 210
Chris@1256 211 // We don't actually support partial cache reads, but let's use
Chris@1256 212 // the term partial to refer to any forward seek and consider a
Chris@1256 213 // backward seek to be a miss
Chris@1256 214 if (start >= m_lastStart) {
Chris@1256 215 lastRead.partial();
Chris@1256 216 } else {
Chris@1256 217 lastRead.miss();
Chris@1256 218 }
Chris@1096 219
Chris@1349 220 if (sf_seek(m_file, start, SEEK_SET) < 0) {
Chris@1096 221 return {};
Chris@148 222 }
Chris@148 223
Chris@1326 224 floatvec_t data;
Chris@1096 225 sv_frame_t n = count * m_fileInfo.channels;
Chris@1096 226 data.resize(n);
Chris@1096 227
Chris@1096 228 m_lastStart = start;
Chris@1096 229 m_lastCount = count;
Chris@1096 230
Chris@1096 231 sf_count_t readCount = 0;
Chris@1349 232 if ((readCount = sf_readf_float(m_file, data.data(), count)) < 0) {
Chris@1096 233 return {};
Chris@1096 234 }
Chris@1096 235
Chris@1103 236 m_buffer = data;
Chris@1096 237 return data;
Chris@148 238 }
Chris@148 239
Chris@1513 240 float
Chris@1513 241 WavFileReader::getMax() const
Chris@1513 242 {
Chris@1513 243 if (!m_file || !m_channelCount) {
Chris@1513 244 return 0.f;
Chris@1513 245 }
Chris@1513 246
Chris@1513 247 // First try for a PEAK chunk
Chris@1513 248
Chris@1513 249 double sfpeak = 0.0;
Chris@1513 250 if (sf_command(m_file, SFC_GET_SIGNAL_MAX, &sfpeak, sizeof(sfpeak))
Chris@1513 251 == SF_TRUE) {
Chris@1513 252 SVDEBUG << "File has a PEAK chunk reporting max level " << sfpeak
Chris@1513 253 << endl;
Chris@1513 254 return float(fabs(sfpeak));
Chris@1513 255 }
Chris@1513 256
Chris@1513 257 // Failing that, read all the samples
Chris@1513 258
Chris@1513 259 float peak = 0.f;
Chris@1513 260 sv_frame_t ix = 0, chunk = 65536;
Chris@1513 261
Chris@1513 262 while (ix < m_frameCount) {
Chris@1513 263 auto frames = getInterleavedFrames(ix, chunk);
Chris@1513 264 for (float x: frames) {
Chris@1513 265 float level = fabsf(x);
Chris@1513 266 if (level > peak) {
Chris@1513 267 peak = level;
Chris@1513 268 }
Chris@1513 269 }
Chris@1513 270 ix += chunk;
Chris@1513 271 }
Chris@1513 272
Chris@1513 273 SVDEBUG << "Measured file peak max level as " << peak << endl;
Chris@1513 274 return peak;
Chris@1513 275 }
Chris@1513 276
Chris@157 277 void
Chris@1096 278 WavFileReader::getSupportedExtensions(set<QString> &extensions)
Chris@157 279 {
Chris@157 280 int count;
Chris@157 281
Chris@1582 282 if (sf_command(nullptr, SFC_GET_FORMAT_MAJOR_COUNT, &count, sizeof(count))) {
Chris@157 283 extensions.insert("wav");
Chris@157 284 extensions.insert("aiff");
Chris@316 285 extensions.insert("aifc");
Chris@157 286 extensions.insert("aif");
Chris@157 287 return;
Chris@157 288 }
Chris@157 289
Chris@157 290 SF_FORMAT_INFO info;
Chris@157 291 for (int i = 0; i < count; ++i) {
Chris@157 292 info.format = i;
Chris@1582 293 if (!sf_command(nullptr, SFC_GET_FORMAT_MAJOR, &info, sizeof(info))) {
Chris@783 294 QString ext = QString(info.extension).toLower();
Chris@783 295 extensions.insert(ext);
Chris@783 296 if (ext == "oga") {
Chris@783 297 // libsndfile is awfully proper, it says it only
Chris@783 298 // supports .oga but lots of Ogg audio files in the
Chris@783 299 // wild are .ogg and it will accept that
Chris@783 300 extensions.insert("ogg");
Chris@783 301 }
Chris@157 302 }
Chris@157 303 }
Chris@157 304 }
Chris@316 305
Chris@316 306 bool
Chris@316 307 WavFileReader::supportsExtension(QString extension)
Chris@316 308 {
Chris@1096 309 set<QString> extensions;
Chris@316 310 getSupportedExtensions(extensions);
Chris@316 311 return (extensions.find(extension.toLower()) != extensions.end());
Chris@316 312 }
Chris@316 313
Chris@316 314 bool
Chris@316 315 WavFileReader::supportsContentType(QString type)
Chris@316 316 {
Chris@316 317 return (type == "audio/x-wav" ||
Chris@316 318 type == "audio/x-aiff" ||
Chris@316 319 type == "audio/basic");
Chris@316 320 }
Chris@316 321
Chris@316 322 bool
Chris@317 323 WavFileReader::supports(FileSource &source)
Chris@316 324 {
Chris@316 325 return (supportsExtension(source.getExtension()) ||
Chris@316 326 supportsContentType(source.getContentType()));
Chris@316 327 }
Chris@316 328
Chris@316 329