From 702cf19f9ecf5d1491e0098ffd31f438f5ec8a26 Mon Sep 17 00:00:00 2001 From: trialuser02 Date: Mon, 18 Jan 2021 18:44:53 +0000 Subject: ffmpeg: added m4b support git-svn-id: http://svn.code.sf.net/p/qmmp-dev/code/trunk/qmmp@9653 90c681e8-e032-0410-971d-27865f9a5e38 --- src/plugins/Input/ffmpeg/decoder_ffmpegm4b.cpp | 231 ++++++++++++++++++++++ src/plugins/Input/ffmpeg/decoder_ffmpegm4b.h | 72 +++++++ src/plugins/Input/ffmpeg/decoderffmpegfactory.cpp | 62 +++++- src/plugins/Input/ffmpeg/decoderffmpegfactory.h | 6 +- src/plugins/Input/ffmpeg/ffmpeg.pro | 6 +- src/plugins/Input/ffmpeg/settingsdialog.cpp | 4 +- 6 files changed, 369 insertions(+), 12 deletions(-) create mode 100644 src/plugins/Input/ffmpeg/decoder_ffmpegm4b.cpp create mode 100644 src/plugins/Input/ffmpeg/decoder_ffmpegm4b.h diff --git a/src/plugins/Input/ffmpeg/decoder_ffmpegm4b.cpp b/src/plugins/Input/ffmpeg/decoder_ffmpegm4b.cpp new file mode 100644 index 000000000..ee3425d72 --- /dev/null +++ b/src/plugins/Input/ffmpeg/decoder_ffmpegm4b.cpp @@ -0,0 +1,231 @@ +/*************************************************************************** + * Copyright (C) 2011-2021 by Ilya Kotov * + * forkotov02@ya.ru * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include +#include +#include +#include +#include +#include "decoder_ffmpeg.h" +#include "decoder_ffmpegm4b.h" + +DecoderFFmpegM4b::DecoderFFmpegM4b(DecoderFactory *factory, const QString &url) : Decoder(), + m_url(url), m_factory(factory) +{} + +DecoderFFmpegM4b::~DecoderFFmpegM4b() +{ + if(m_decoder) + delete m_decoder; + m_decoder = nullptr; + if(m_buf) + delete [] m_buf; + m_buf = nullptr; + if(m_input) + m_input->deleteLater(); + m_input = nullptr; + + for(ChapterInfo &ch : m_chapters) + { + delete ch.info; + ch.info = nullptr; + } +} + +bool DecoderFFmpegM4b::initialize() +{ + QString filePath = m_url; + if(!m_url.startsWith("m4b://")) + { + qWarning("DecoderFFmpegM4b: invalid url."); + return false; + } + filePath.remove("m4b://"); + filePath.remove(QRegularExpression("#\\d+$")); + m_track = m_url.section("#", -1).toInt(); + + AVFormatContext *in = nullptr; +#ifdef Q_OS_WIN + if(avformat_open_input(&in, filePath.toUtf8().constData(), nullptr, nullptr) < 0) +#else + if(avformat_open_input(&in, filePath.toLocal8Bit().constData(), nullptr, nullptr) < 0) +#endif + { + qDebug("DecoderFFmpegM4b: unable to open file"); + return false; + } + + avformat_find_stream_info(in, nullptr); + + if(in->nb_chapters <= 1) + { + avformat_close_input(&in); + qWarning("DecoderFFmpegM4b: unable to find chapters"); + return false; + } + + if(m_track > int(in->nb_chapters) || m_track < 1) + { + avformat_close_input(&in); + qWarning("DecoderFFmpegM4b: invalid track number"); + return false; + } + + QList tracks = m_factory->createPlayList(filePath, TrackInfo::AllParts, nullptr); + if(tracks.isEmpty() || tracks.count() != int(in->nb_chapters)) + { + qDeleteAll(tracks); + avformat_close_input(&in); + qWarning("DecoderFFmpegM4b: unable to find tracks"); + return false; + } + + for(int i = 0; i < tracks.count(); ++i) + { + AVChapter *chapter = in->chapters[i]; + ChapterInfo chapterInfo = { + .info = tracks[i], + .offset = chapter->start * chapter->time_base.num * 1000 / chapter->time_base.den, + .duration = (chapter->end - chapter->start) * chapter->time_base.num * 1000 / chapter->time_base.den, + .url = QString("m4b://%1#%2").arg(filePath).arg(i + 1) + }; + + m_chapters << chapterInfo; + } + + tracks.clear(); + avformat_close_input(&in); + + m_input = new QFile(filePath); + if(!m_input->open(QIODevice::ReadOnly)) + { + qWarning("DecoderFFmpegM4b: unable to open file; error: %s", qPrintable(m_input->errorString())); + return false; + } + + m_duration = m_chapters[m_track - 1].duration; + m_offset = m_chapters[m_track - 1].offset; + m_decoder = new DecoderFFmpeg(filePath, m_input); + if(!m_decoder->initialize()) + { + qDeleteAll(tracks); + qWarning("DecoderFFapCUE: invalid audio file"); + return false; + } + m_decoder->seek(m_offset); + + configure(m_decoder->audioParameters()); + + m_trackSize = audioParameters().sampleRate() * audioParameters().channels() * + audioParameters().sampleSize() * m_duration / 1000; + m_written = 0; + + m_frameSize = audioParameters().sampleSize() * audioParameters().channels(); + + setReplayGainInfo(m_decoder->replayGainInfo()); //send ReplayGaing info + addMetaData(m_chapters[m_track - 1].info->metaData()); //send metadata + return true; +} + +qint64 DecoderFFmpegM4b::totalTime() const +{ + return m_decoder ? m_duration : 0; +} + +void DecoderFFmpegM4b::seek(qint64 pos) +{ + m_decoder->seek(m_offset + pos); + m_written = audioParameters().sampleRate() * + audioParameters().channels() * + audioParameters().sampleSize() * pos/1000; +} + +qint64 DecoderFFmpegM4b::read(unsigned char *data, qint64 size) +{ + if(m_trackSize - m_written < m_frameSize) //end of cue track + return 0; + + qint64 len = 0; + + if(m_buf) //read remaining data first + { + len = qMin(m_bufSize, size); + memmove(data, m_buf, len); + if(size >= m_bufSize) + { + delete[] m_buf; + m_buf = nullptr; + m_bufSize = 0; + } + else + memmove(m_buf, m_buf + len, size - len); + } + else + len = m_decoder->read(data, size); + + if(len <= 0) //end of file + return 0; + + if(len + m_written <= m_trackSize) + { + m_written += len; + return len; + } + + qint64 len2 = qMax(qint64(0), m_trackSize - m_written); + len2 = (len2 / m_frameSize) * m_frameSize; //integer number of samples + m_written += len2; + //save data of the next track + if(m_buf) + delete[] m_buf; + m_bufSize = len - len2; + m_buf = new char[m_bufSize]; + memmove(m_buf, data + len2, m_bufSize); + return len2; +} + +int DecoderFFmpegM4b::bitrate() const +{ + return m_decoder->bitrate(); +} + +const QString DecoderFFmpegM4b::nextURL() const +{ + if(m_track + 1 <= m_chapters.count()) + return m_chapters[m_track].url; + else + return QString(); +} + +void DecoderFFmpegM4b::next() +{ + if(m_track + 1 <= m_chapters.count()) + { + m_track++; + m_duration = m_chapters[m_track - 1].duration; + m_offset = m_chapters[m_track - 1].offset; + m_trackSize = audioParameters().sampleRate() * + audioParameters().channels() * + audioParameters().sampleSize() * m_duration/1000; + addMetaData(m_chapters[m_track - 1].info->metaData()); + setReplayGainInfo(m_decoder->replayGainInfo()); + m_written = 0; + } +} diff --git a/src/plugins/Input/ffmpeg/decoder_ffmpegm4b.h b/src/plugins/Input/ffmpeg/decoder_ffmpegm4b.h new file mode 100644 index 000000000..3d6081a55 --- /dev/null +++ b/src/plugins/Input/ffmpeg/decoder_ffmpegm4b.h @@ -0,0 +1,72 @@ +/*************************************************************************** + * Copyright (C) 2011-2021 by Ilya Kotov * + * forkotov02@ya.ru * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef DECODER_FFMPEGM4B_H +#define DECODER_FFMPEGM4B_H + +#include + +class TrackInfo; +class QIDevice; + +/** + @author Ilya Kotov +*/ +class DecoderFFmpegM4b : public Decoder +{ +public: + explicit DecoderFFmpegM4b(DecoderFactory *factory, const QString &url); + virtual ~DecoderFFmpegM4b(); + + // Standard Decoder API + bool initialize() override; + qint64 totalTime() const override; + void seek(qint64) override; + qint64 read(unsigned char *data, qint64 size) override; + int bitrate() const override; + const QString nextURL() const override; + void next() override; + +private: + Decoder *m_decoder = nullptr; + char *m_buf = nullptr; //buffer for remainig data + int m_track = 0, m_count = 0; + qint64 m_duration = 0; + qint64 m_offset = 0; + qint64 m_trackSize = 0; + qint64 m_written = 0; + QString m_url; + qint64 m_bufSize = 0; + qint64 m_frameSize = 0; //sample size + QIODevice *m_input = nullptr; + DecoderFactory *m_factory; + + struct ChapterInfo + { + TrackInfo *info; + qint64 offset; + qint64 duration; + QString url; + }; + + QList m_chapters; +}; + +#endif // DECODER_FFMPEGM4B_H diff --git a/src/plugins/Input/ffmpeg/decoderffmpegfactory.cpp b/src/plugins/Input/ffmpeg/decoderffmpegfactory.cpp index 6c5c1c53a..040819ba9 100644 --- a/src/plugins/Input/ffmpeg/decoderffmpegfactory.cpp +++ b/src/plugins/Input/ffmpeg/decoderffmpegfactory.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include extern "C"{ #include @@ -34,6 +35,7 @@ extern "C"{ #include "settingsdialog.h" #include "decoder_ffmpeg.h" #include "decoder_ffmpegcue.h" +#include "decoder_ffmpegm4b.h" #include "decoderffmpegfactory.h" @@ -96,10 +98,13 @@ DecoderProperties DecoderFFmpegFactory::properties() const { QSettings settings(Qmmp::configFile(), QSettings::IniFormat); QStringList filters = { - "*.wma", "*.ape", "*.tta", "*.m4a", "*.aac", "*.ra", "*.shn", "*.vqf", "*.ac3", "*.tak", "*.dsf", "*.dsdiff" + "*.wma", "*.ape", "*.tta", "*.m4a", "*.m4b", "*.aac", "*.ra", "*.shn", "*.vqf", "*.ac3", "*.tak", "*.dsf", "*.dsdiff" }; filters = settings.value("FFMPEG/filters", filters).toStringList(); + if(filters.contains("*.m4a") && !filters.contains("*.m4b")) + filters << "*.m4b"; + if(!avcodec_find_decoder(AV_CODEC_ID_WMAV1)) filters.removeAll("*.wma"); if(!avcodec_find_decoder(AV_CODEC_ID_APE)) @@ -111,7 +116,10 @@ DecoderProperties DecoderFFmpegFactory::properties() const if(!avcodec_find_decoder(AV_CODEC_ID_MP3)) filters.removeAll("*.mp3"); if(!avcodec_find_decoder(AV_CODEC_ID_AAC) && !avcodec_find_decoder(AV_CODEC_ID_ALAC)) + { filters.removeAll("*.m4a"); + filters.removeAll("*.m4b"); + } if(!avcodec_find_decoder(AV_CODEC_ID_RA_288)) filters.removeAll("*.ra"); if(!avcodec_find_decoder(AV_CODEC_ID_SHORTEN)) @@ -160,7 +168,7 @@ DecoderProperties DecoderFFmpegFactory::properties() const properties.hasAbout = true; properties.hasSettings = true; properties.noInput = false; - properties.protocols << "ffmpeg"; + properties.protocols << "ffmpeg" << "m4b"; properties.priority = 10; return properties; } @@ -169,24 +177,28 @@ Decoder *DecoderFFmpegFactory::create(const QString &path, QIODevice *input) { if(path.startsWith("ffmpeg://")) return new DecoderFFmpegCue(path); + else if(path.startsWith("m4b://")) + return new DecoderFFmpegM4b(this, path); else return new DecoderFFmpeg(path, input); } QList DecoderFFmpegFactory::createPlayList(const QString &path, TrackInfo::Parts parts, QStringList *) { - int cueTrack = -1; //cue track + qDebug() << path; + int trackNumber = -1; //cue/m4b track QString filePath = path; if(path.contains("://")) //is it cue track? { filePath.remove("ffmpeg://"); + filePath.remove("m4b://"); filePath.remove(QRegularExpression("#\\d+$")); - cueTrack = path.section("#", -1).toInt(); - parts = TrackInfo::AllParts; //extract all metadata for single cue track + trackNumber = path.section("#", -1).toInt(); + parts = TrackInfo::AllParts; //extract all metadata for single cue/m4b track } - TrackInfo *info = new TrackInfo(filePath); + TrackInfo *info = new TrackInfo(filePath); if(parts == TrackInfo::Parts()) return QList() << info; @@ -241,7 +253,7 @@ QList DecoderFFmpegFactory::createPlayList(const QString &path, Tra avformat_close_input(&in); delete info; - return (cueTrack > 0) ? parser.createPlayList(cueTrack) : parser.createPlayList(); + return (trackNumber > 0) ? parser.createPlayList(trackNumber) : parser.createPlayList(); } AVDictionaryEntry *album = av_dict_get(in->metadata,"album",nullptr,0); @@ -288,6 +300,14 @@ QList DecoderFFmpegFactory::createPlayList(const QString &path, Tra info->setValue(Qmmp::YEAR, year->value); if(track) info->setValue(Qmmp::TRACK, track->value); + + if(in->nb_chapters > 1 && filePath.endsWith(".m4b", Qt::CaseInsensitive)) + { + QList tracks = createPlayListFromChapters(in, info, trackNumber); + avformat_close_input(&in); + delete info; + return tracks; + } } avformat_close_input(&in); @@ -330,3 +350,31 @@ QString DecoderFFmpegFactory::translation() const { return QLatin1String(":/ffmpeg_plugin_"); } + +QList DecoderFFmpegFactory::createPlayListFromChapters(AVFormatContext *in, + TrackInfo *extraInfo, + int trackNumber) +{ + QList tracks; + + for(unsigned int i = 0; i < in->nb_chapters; ++i) + { + if((trackNumber > 0) && (int(i + 1) != trackNumber)) + continue; + + AVChapter *chapter = in->chapters[i]; + TrackInfo *info = new TrackInfo(QString("m4b://%1#%2").arg(extraInfo->path()).arg(i + 1)); + info->setDuration((chapter->end - chapter->start) * av_q2d(chapter->time_base) * 1000); + info->setValues(extraInfo->properties()); + info->setValues(extraInfo->metaData()); + info->setValue(Qmmp::TRACK, i + 1); + + AVDictionaryEntry *title = av_dict_get(chapter->metadata,"title", nullptr, 0); + if(title) + info->setValue(Qmmp::TITLE, QString::fromUtf8(title->value).trimmed()); + + tracks << info; + } + + return tracks; +} diff --git a/src/plugins/Input/ffmpeg/decoderffmpegfactory.h b/src/plugins/Input/ffmpeg/decoderffmpegfactory.h index f6abfd8a3..b95ad70cc 100644 --- a/src/plugins/Input/ffmpeg/decoderffmpegfactory.h +++ b/src/plugins/Input/ffmpeg/decoderffmpegfactory.h @@ -20,9 +20,10 @@ #ifndef DECODERFFMPEGFACTORY_H #define DECODERFFMPEGFACTORY_H - #include +struct AVFormatContext; + class DecoderFFmpegFactory : public QObject, DecoderFactory { Q_OBJECT @@ -39,6 +40,9 @@ public: void showSettings(QWidget *parent) override; void showAbout(QWidget *parent) override; QString translation() const override; + +private: + QList createPlayListFromChapters(AVFormatContext *in, TrackInfo *info, int trackNumber); }; #endif diff --git a/src/plugins/Input/ffmpeg/ffmpeg.pro b/src/plugins/Input/ffmpeg/ffmpeg.pro index 8efc547db..f656b23f9 100644 --- a/src/plugins/Input/ffmpeg/ffmpeg.pro +++ b/src/plugins/Input/ffmpeg/ffmpeg.pro @@ -7,14 +7,16 @@ HEADERS += decoderffmpegfactory.h \ settingsdialog.h \ ffmpegmetadatamodel.h \ replaygainreader.h \ - decoder_ffmpegcue.h + decoder_ffmpegcue.h \ + decoder_ffmpegm4b.h SOURCES += decoder_ffmpeg.cpp \ decoderffmpegfactory.cpp \ settingsdialog.cpp \ ffmpegmetadatamodel.cpp \ replaygainreader.cpp \ - decoder_ffmpegcue.cpp + decoder_ffmpegcue.cpp \ + decoder_ffmpegm4b.cpp FORMS += settingsdialog.ui diff --git a/src/plugins/Input/ffmpeg/settingsdialog.cpp b/src/plugins/Input/ffmpeg/settingsdialog.cpp index 933467b53..957a63b16 100644 --- a/src/plugins/Input/ffmpeg/settingsdialog.cpp +++ b/src/plugins/Input/ffmpeg/settingsdialog.cpp @@ -36,7 +36,7 @@ SettingsDialog::SettingsDialog(QWidget *parent) setAttribute(Qt::WA_DeleteOnClose); QSettings settings(Qmmp::configFile(), QSettings::IniFormat); QStringList filters = { - "*.wma", "*.ape", "*.tta", "*.m4a", "*.aac", "*.ra", "*.shn", "*.vqf", "*.ac3", "*.tak", "*.dsf", "*.dsdiff" + "*.wma", "*.ape", "*.tta", "*.m4a", "*.m4b", "*.aac", "*.ra", "*.shn", "*.vqf", "*.ac3", "*.tak", "*.dsf", "*.dsdiff" }; filters = settings.value("FFMPEG/filters", filters).toStringList(); @@ -88,7 +88,7 @@ void SettingsDialog::accept() if (m_ui.aacCheckBox->isChecked()) filters << "*.aac"; if (m_ui.mp4CheckBox->isChecked()) - filters << "*.m4a"; + filters << "*.m4a" << "*.m4b"; if (m_ui.raCheckBox->isChecked()) filters << "*.ra"; if (m_ui.shCheckBox->isChecked()) -- cgit v1.2.3-13-gbd6f