Chris@148: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@148: Chris@148: /* Chris@148: Sonic Visualiser Chris@148: An audio file viewer and annotation editor. Chris@148: Centre for Digital Music, Queen Mary, University of London. Chris@202: This file copyright 2006 Chris Cannam and QMUL. Chris@148: Chris@148: This program is free software; you can redistribute it and/or Chris@148: modify it under the terms of the GNU General Public License as Chris@148: published by the Free Software Foundation; either version 2 of the Chris@148: License, or (at your option) any later version. See the file Chris@148: COPYING included with this distribution for more information. Chris@148: */ Chris@148: Chris@148: #include "WavFileReader.h" Chris@148: Chris@1256: #include "base/HitCount.h" Chris@1258: #include "base/Profiler.h" Chris@1256: Chris@148: #include Chris@148: Chris@175: #include Chris@316: #include Chris@175: Chris@1096: using namespace std; Chris@1096: Chris@1513: WavFileReader::WavFileReader(FileSource source, Chris@1513: bool fileUpdating, Chris@1520: Normalisation normalisation) : Chris@1349: m_file(0), Chris@316: m_source(source), Chris@316: m_path(source.getLocalFilename()), Chris@823: m_seekable(false), Chris@148: m_lastStart(0), Chris@176: m_lastCount(0), Chris@1520: m_normalisation(normalisation), Chris@1513: m_max(0.f), Chris@176: m_updating(fileUpdating) Chris@148: { Chris@148: m_frameCount = 0; Chris@148: m_channelCount = 0; Chris@148: m_sampleRate = 0; Chris@148: Chris@148: m_fileInfo.format = 0; Chris@148: m_fileInfo.frames = 0; Chris@148: Chris@1349: #ifdef Q_OS_WIN Chris@1349: m_file = sf_wchar_open((LPCWSTR)m_path.utf16(), SFM_READ, &m_fileInfo); Chris@1349: #else Chris@1349: m_file = sf_open(m_path.toLocal8Bit(), SFM_READ, &m_fileInfo); Chris@1349: #endif Chris@1349: Chris@1349: if (!m_file || (!fileUpdating && m_fileInfo.channels <= 0)) { Chris@1343: SVDEBUG << "WavFileReader::initialize: Failed to open file at \"" Chris@1349: << m_path << "\" (" Chris@1349: << sf_strerror(m_file) << ")" << endl; Chris@148: Chris@1349: if (m_file) { Chris@1343: m_error = QString("Couldn't load audio file '%1':\n%2") Chris@1349: .arg(m_path).arg(sf_strerror(m_file)); Chris@1343: } else { Chris@1349: m_error = QString("Failed to open audio file '%1'") Chris@1349: .arg(m_path); Chris@1343: } Chris@1343: return; Chris@148: } Chris@148: Chris@187: if (m_fileInfo.channels > 0) { Chris@823: Chris@187: m_frameCount = m_fileInfo.frames; Chris@187: m_channelCount = m_fileInfo.channels; Chris@187: m_sampleRate = m_fileInfo.samplerate; Chris@823: Chris@823: m_seekable = (m_fileInfo.seekable != 0); Chris@823: Chris@823: int type = m_fileInfo.format & SF_FORMAT_TYPEMASK; Chris@1162: int subtype = m_fileInfo.format & SF_FORMAT_SUBMASK; Chris@1162: Chris@823: if (type >= SF_FORMAT_FLAC || type >= SF_FORMAT_OGG) { Chris@1162: // Our m_seekable reports whether a file is rapidly Chris@1162: // seekable, so things like Ogg don't qualify. We Chris@1162: // cautiously report every file type of "at least" the Chris@1162: // historical period of Ogg or FLAC as non-seekable. Chris@823: m_seekable = false; Chris@1162: } else if (type == SF_FORMAT_WAV && subtype <= SF_FORMAT_DOUBLE) { Chris@1162: // libsndfile 1.0.26 has a bug (subsequently fixed in the Chris@1162: // repo) that causes all files to be reported as Chris@1162: // non-seekable. We know that certain common file types Chris@1162: // are definitely seekable so, again cautiously, identify Chris@1162: // and mark those (basically only non-adaptive WAVs). Chris@1162: m_seekable = true; Chris@823: } Chris@1513: Chris@1520: if (m_normalisation != Normalisation::None && !m_updating) { Chris@1513: m_max = getMax(); Chris@1513: } Chris@1592: Chris@1592: const char *str = sf_get_string(m_file, SF_STR_TITLE); Chris@1592: if (str) { Chris@1592: m_title = str; Chris@1592: } Chris@1592: str = sf_get_string(m_file, SF_STR_ARTIST); Chris@1592: if (str) { Chris@1592: m_maker = str; Chris@1592: } Chris@187: } Chris@175: Chris@1520: 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: } Chris@148: Chris@148: WavFileReader::~WavFileReader() Chris@148: { Chris@1349: if (m_file) sf_close(m_file); Chris@148: } Chris@148: Chris@148: void Chris@175: WavFileReader::updateFrameCount() Chris@175: { Chris@175: QMutexLocker locker(&m_mutex); Chris@175: Chris@1038: sv_frame_t prevCount = m_fileInfo.frames; Chris@175: Chris@1349: if (m_file) { Chris@1349: sf_close(m_file); Chris@1349: #ifdef Q_OS_WIN Chris@1349: m_file = sf_wchar_open((LPCWSTR)m_path.utf16(), SFM_READ, &m_fileInfo); Chris@1349: #else Chris@1349: m_file = sf_open(m_path.toLocal8Bit(), SFM_READ, &m_fileInfo); Chris@1349: #endif Chris@1349: if (!m_file || m_fileInfo.channels <= 0) { Chris@1349: SVDEBUG << "WavFileReader::updateFrameCount: Failed to open file at \"" << m_path << "\" (" Chris@1349: << sf_strerror(m_file) << ")" << endl; Chris@175: } Chris@175: } Chris@175: Chris@690: // SVDEBUG << "WavFileReader::updateFrameCount: now " << m_fileInfo.frames << endl; Chris@175: Chris@176: m_frameCount = m_fileInfo.frames; Chris@176: Chris@187: if (m_channelCount == 0) { Chris@187: m_channelCount = m_fileInfo.channels; Chris@187: m_sampleRate = m_fileInfo.samplerate; Chris@187: } Chris@187: Chris@258: if (m_frameCount != prevCount) { Chris@258: emit frameCountChanged(); Chris@258: } Chris@176: } Chris@176: Chris@176: void Chris@176: WavFileReader::updateDone() Chris@176: { Chris@176: updateFrameCount(); Chris@176: m_updating = false; Chris@1520: if (m_normalisation != Normalisation::None) { Chris@1513: m_max = getMax(); Chris@1513: } Chris@175: } Chris@175: Chris@1326: floatvec_t Chris@1041: WavFileReader::getInterleavedFrames(sv_frame_t start, sv_frame_t count) const Chris@148: { Chris@1513: floatvec_t frames = getInterleavedFramesUnnormalised(start, count); Chris@1513: Chris@1520: if (m_normalisation == Normalisation::None || m_max == 0.f) { Chris@1513: return frames; Chris@1513: } Chris@1513: Chris@1513: for (int i = 0; in_range_for(frames, i); ++i) { Chris@1513: frames[i] /= m_max; Chris@1513: } Chris@1513: Chris@1513: return frames; Chris@1513: } Chris@1513: Chris@1513: floatvec_t Chris@1513: WavFileReader::getInterleavedFramesUnnormalised(sv_frame_t start, Chris@1513: sv_frame_t count) const Chris@1513: { Chris@1256: static HitCount lastRead("WavFileReader: last read"); Chris@1256: Chris@1096: if (count == 0) return {}; Chris@175: Chris@175: QMutexLocker locker(&m_mutex); Chris@175: Chris@1258: Profiler profiler("WavFileReader::getInterleavedFrames"); Chris@1258: Chris@1349: if (!m_file || !m_channelCount) { Chris@1096: return {}; Chris@175: } Chris@148: Chris@1040: if (start >= m_fileInfo.frames) { Chris@690: // SVDEBUG << "WavFileReader::getInterleavedFrames: " << start Chris@687: // << " > " << m_fileInfo.frames << endl; Chris@1343: return {}; Chris@148: } Chris@148: Chris@1040: if (start + count > m_fileInfo.frames) { Chris@1343: count = m_fileInfo.frames - start; Chris@148: } Chris@148: Chris@1096: // Because WaveFileModel::getSummaries() is called separately for Chris@1096: // individual channels, it's quite common for us to be called Chris@1096: // repeatedly for the same data. So this is worth cacheing. Chris@1096: if (start == m_lastStart && count == m_lastCount) { Chris@1256: lastRead.hit(); Chris@1096: return m_buffer; Chris@1096: } Chris@1256: Chris@1256: // We don't actually support partial cache reads, but let's use Chris@1256: // the term partial to refer to any forward seek and consider a Chris@1256: // backward seek to be a miss Chris@1256: if (start >= m_lastStart) { Chris@1256: lastRead.partial(); Chris@1256: } else { Chris@1256: lastRead.miss(); Chris@1256: } Chris@1096: Chris@1349: if (sf_seek(m_file, start, SEEK_SET) < 0) { Chris@1096: return {}; Chris@148: } Chris@148: Chris@1326: floatvec_t data; Chris@1096: sv_frame_t n = count * m_fileInfo.channels; Chris@1096: data.resize(n); Chris@1096: Chris@1096: m_lastStart = start; Chris@1096: m_lastCount = count; Chris@1096: Chris@1096: sf_count_t readCount = 0; Chris@1349: if ((readCount = sf_readf_float(m_file, data.data(), count)) < 0) { Chris@1096: return {}; Chris@1096: } Chris@1096: Chris@1103: m_buffer = data; Chris@1096: return data; Chris@148: } Chris@148: Chris@1513: float Chris@1513: WavFileReader::getMax() const Chris@1513: { Chris@1513: if (!m_file || !m_channelCount) { Chris@1513: return 0.f; Chris@1513: } Chris@1513: Chris@1513: // First try for a PEAK chunk Chris@1513: Chris@1513: double sfpeak = 0.0; Chris@1513: if (sf_command(m_file, SFC_GET_SIGNAL_MAX, &sfpeak, sizeof(sfpeak)) Chris@1513: == SF_TRUE) { Chris@1513: SVDEBUG << "File has a PEAK chunk reporting max level " << sfpeak Chris@1513: << endl; Chris@1513: return float(fabs(sfpeak)); Chris@1513: } Chris@1513: Chris@1513: // Failing that, read all the samples Chris@1513: Chris@1513: float peak = 0.f; Chris@1513: sv_frame_t ix = 0, chunk = 65536; Chris@1513: Chris@1513: while (ix < m_frameCount) { Chris@1513: auto frames = getInterleavedFrames(ix, chunk); Chris@1513: for (float x: frames) { Chris@1513: float level = fabsf(x); Chris@1513: if (level > peak) { Chris@1513: peak = level; Chris@1513: } Chris@1513: } Chris@1513: ix += chunk; Chris@1513: } Chris@1513: Chris@1513: SVDEBUG << "Measured file peak max level as " << peak << endl; Chris@1513: return peak; Chris@1513: } Chris@1513: Chris@157: void Chris@1096: WavFileReader::getSupportedExtensions(set &extensions) Chris@157: { Chris@157: int count; Chris@157: Chris@157: if (sf_command(0, SFC_GET_FORMAT_MAJOR_COUNT, &count, sizeof(count))) { Chris@157: extensions.insert("wav"); Chris@157: extensions.insert("aiff"); Chris@316: extensions.insert("aifc"); Chris@157: extensions.insert("aif"); Chris@157: return; Chris@157: } Chris@157: Chris@157: SF_FORMAT_INFO info; Chris@157: for (int i = 0; i < count; ++i) { Chris@157: info.format = i; Chris@157: if (!sf_command(0, SFC_GET_FORMAT_MAJOR, &info, sizeof(info))) { Chris@783: QString ext = QString(info.extension).toLower(); Chris@783: extensions.insert(ext); Chris@783: if (ext == "oga") { Chris@783: // libsndfile is awfully proper, it says it only Chris@783: // supports .oga but lots of Ogg audio files in the Chris@783: // wild are .ogg and it will accept that Chris@783: extensions.insert("ogg"); Chris@783: } Chris@157: } Chris@157: } Chris@157: } Chris@316: Chris@316: bool Chris@316: WavFileReader::supportsExtension(QString extension) Chris@316: { Chris@1096: set extensions; Chris@316: getSupportedExtensions(extensions); Chris@316: return (extensions.find(extension.toLower()) != extensions.end()); Chris@316: } Chris@316: Chris@316: bool Chris@316: WavFileReader::supportsContentType(QString type) Chris@316: { Chris@316: return (type == "audio/x-wav" || Chris@316: type == "audio/x-aiff" || Chris@316: type == "audio/basic"); Chris@316: } Chris@316: Chris@316: bool Chris@317: WavFileReader::supports(FileSource &source) Chris@316: { Chris@316: return (supportsExtension(source.getExtension()) || Chris@316: supportsContentType(source.getContentType())); Chris@316: } Chris@316: Chris@316: