From df77c27641e688b8ce3cb5de0f7bb4150142836c Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Fri, 29 Nov 2024 21:07:23 +0800 Subject: [PATCH] QMPlay2: legacy version: fix YouTube search, backport bit-perfect audio --- multimedia/QMPlay2/Portfile | 24 +- .../QMPlay2/files/0007-Fix-YT-search.patch | 414 ++++++ .../QMPlay2/files/0008-Switch-to-yt-dlp.patch | 85 ++ ...bit-perfect-audio-output-for-macOS-1.patch | 1207 +++++++++++++++++ ...ching-yt-dlp-it-does-not-work-use-Ma.patch | 61 + .../files/0011-YouTubeDL-move-to-QJson.patch | 88 ++ ...Qt-paths.patch => 0012-Fix-Qt-paths.patch} | 0 7 files changed, 1876 insertions(+), 3 deletions(-) create mode 100644 multimedia/QMPlay2/files/0007-Fix-YT-search.patch create mode 100644 multimedia/QMPlay2/files/0008-Switch-to-yt-dlp.patch create mode 100644 multimedia/QMPlay2/files/0009-PortAudio-Allow-bit-perfect-audio-output-for-macOS-1.patch create mode 100644 multimedia/QMPlay2/files/0010-Do-not-force-fetching-yt-dlp-it-does-not-work-use-Ma.patch create mode 100644 multimedia/QMPlay2/files/0011-YouTubeDL-move-to-QJson.patch rename multimedia/QMPlay2/files/{0007-Fix-Qt-paths.patch => 0012-Fix-Qt-paths.patch} (100%) diff --git a/multimedia/QMPlay2/Portfile b/multimedia/QMPlay2/Portfile index adca8308f22f8..3e2aee1897ba9 100644 --- a/multimedia/QMPlay2/Portfile +++ b/multimedia/QMPlay2/Portfile @@ -18,6 +18,12 @@ if {${os.platform} eq "darwin" && ${os.major} < 15} { size 1228156 github.tarball_from archive + # These are added to support YouTube: + depends_lib-append \ + port:qjson4 + depends_run-append \ + port:yt-dlp + patchfiles-append \ 0001-macOS-unbreak-build-with-Qt4.patch \ 0002-MainWidget-fix-for-missing-qt_mac_set_dock_menu.patch \ @@ -25,14 +31,19 @@ if {${os.platform} eq "darwin" && ${os.major} < 15} { 0004-PlayClass-do-not-use-QRawFont.patch \ 0005-Revert-some-broken-code.patch \ 0006-FFMpeg-fix-compatibility-with-modern-FFMpeg.patch \ - 0007-Fix-Qt-paths.patch + 0007-Fix-YT-search.patch \ + 0008-Switch-to-yt-dlp.patch \ + 0009-PortAudio-Allow-bit-perfect-audio-output-for-macOS-1.patch \ + 0010-Do-not-force-fetching-yt-dlp-it-does-not-work-use-Ma.patch \ + 0011-YouTubeDL-move-to-QJson.patch \ + 0012-Fix-Qt-paths.patch configure.args-append \ -DDEFAULT_QT5=OFF \ + -DUSE_FFMPEG_VTB=OFF \ -DUSE_JEMALLOC=OFF \ -DUSE_OPENGL2=OFF \ - -DUSE_TEKSTOWO=ON \ - -DUSE_FFMPEG_VTB=OFF + -DUSE_TEKSTOWO=ON compiler.cxx_standard 2011 @@ -51,6 +62,13 @@ if {${os.platform} eq "darwin" && ${os.major} < 15} { } } + notes " + ${name} expects to have yt-dlp in ~/.qmplay2/ and use it for YouTube playback.\ + After installation you could make a symlink to MacPorts-provided yt-dlp:\ + \ + ln -s ${prefix}/bin/yt-dlp ~/.qmplay/ + " + variant jemalloc description "Use Jemalloc" { depends_lib-append \ port:jemalloc diff --git a/multimedia/QMPlay2/files/0007-Fix-YT-search.patch b/multimedia/QMPlay2/files/0007-Fix-YT-search.patch new file mode 100644 index 0000000000000..eb9c52917d85c --- /dev/null +++ b/multimedia/QMPlay2/files/0007-Fix-YT-search.patch @@ -0,0 +1,414 @@ +From 176915efac285fe23935db666771af3b93f3bbfc Mon Sep 17 00:00:00 2001 +From: Sergey Fedorov +Date: Tue, 19 Nov 2024 15:09:39 +0800 +Subject: [PATCH 07/11] Fix YT search + +--- + src/modules/Extensions/CMakeLists.txt | 3 +- + src/modules/Extensions/YouTube.cpp | 261 ++++++++++++-------------- + src/modules/Extensions/YouTube.hpp | 7 +- + src/qmplay2/Functions.cpp | 31 +++ + src/qmplay2/headers/Functions.hpp | 2 + + 5 files changed, 157 insertions(+), 147 deletions(-) + +diff --git src/modules/Extensions/CMakeLists.txt src/modules/Extensions/CMakeLists.txt +index a56ceea4..6f69bb81 100644 +--- src/modules/Extensions/CMakeLists.txt ++++ src/modules/Extensions/CMakeLists.txt +@@ -117,7 +117,8 @@ add_library(${PROJECT_NAME} ${QMPLAY2_MODULE} + if(USE_QT5) + qt5_use_modules(${PROJECT_NAME} Gui Widgets ${DBUS}) + else() +- target_link_libraries(${PROJECT_NAME} Qt4::QtCore Qt4::QtGui ${DBUS}) ++ add_definitions(-I/opt/local/include/QJson4) ++ target_link_libraries(${PROJECT_NAME} Qt4::QtCore Qt4::QtGui QJson4 ${DBUS}) + endif() + + add_dependencies(${PROJECT_NAME} libqmplay2) +diff --git src/modules/Extensions/YouTube.cpp src/modules/Extensions/YouTube.cpp +index 4e71605b..73924de2 100644 +--- src/modules/Extensions/YouTube.cpp ++++ src/modules/Extensions/YouTube.cpp +@@ -26,6 +26,9 @@ + #include + #include + #include ++#include ++#include ++#include + #include + #include + #include +@@ -819,133 +822,94 @@ void YouTube::setAutocomplete(const QByteArray &data) + completer->complete(); + } + } +-void YouTube::setSearchResults(QString data) ++void YouTube::setSearchResults(const QByteArray &data) + { +- /* Usuwanie komentarzy HTML */ +- for (int commentIdx = 0 ;;) ++ const auto json = getYtInitialData(data); ++ const auto contents = json.object() ++ ["contents"].toObject() ++ ["twoColumnSearchResultsRenderer"].toObject() ++ ["primaryContents"].toObject() ++ ["sectionListRenderer"].toObject() ++ ["contents"].toArray().at(0).toObject() ++ ["itemSectionRenderer"].toObject() ++ ["contents"].toArray() ++ ; ++ for (auto &&obj : contents) + { +- if ((commentIdx = data.indexOf("", commentIdx); +- if (commentEndIdx >= 0) //Jeżeli jest koniec komentarza +- data.remove(commentIdx, commentEndIdx - commentIdx + 3); //Wyrzuć zakomentowany fragment +- else +- { +- data.remove(commentIdx, data.length() - commentIdx); //Wyrzuć cały tekst do końca +- break; +- } +- } ++ const auto videoRenderer = obj.toObject()["videoRenderer"].toObject(); ++ const auto playlistRenderer = obj.toObject()["playlistRenderer"].toObject(); + +- int i; +- const QStringList splitted = data.split("yt-lockup "); +- for (i = 1; i < splitted.count(); ++i) +- { +- QString title, videoInfoLink, duration, image, user; +- const QString &entry = splitted[i]; +- int idx; ++ const bool isVideo = !videoRenderer.isEmpty() && playlistRenderer.isEmpty(); + +- if (entry.contains("yt-lockup-channel")) //Ignore channels +- continue; ++ QString title, contentId, length, user, publishedTime, viewCount, thumbnail, url; + +- const bool isPlaylist = entry.contains("yt-lockup-playlist"); +- +- if ((idx = entry.indexOf("yt-lockup-title")) > -1) ++ if (isVideo) + { +- int urlIdx = entry.indexOf("href=\"", idx); +- int titleIdx = entry.indexOf("title=\"", idx); +- if (titleIdx > -1 && urlIdx > -1 && titleIdx > urlIdx) +- { +- const int endUrlIdx = entry.indexOf("\"", urlIdx += 6); +- const int endTitleIdx = entry.indexOf("\"", titleIdx += 7); +- if (endTitleIdx > -1 && endUrlIdx > -1 && endTitleIdx > endUrlIdx) +- { +- videoInfoLink = entry.mid(urlIdx, endUrlIdx - urlIdx).replace("&", "&"); +- if (!videoInfoLink.isEmpty() && videoInfoLink.startsWith('/')) +- videoInfoLink.prepend(YOUTUBE_URL); +- title = entry.mid(titleIdx, endTitleIdx - titleIdx); +- } +- } ++ title = videoRenderer["title"].toObject()["runs"].toArray().at(0).toObject()["text"].toString(); ++ contentId = videoRenderer["videoId"].toString(); ++ if (title.isEmpty() || contentId.isEmpty()) ++ continue; ++ length = videoRenderer["lengthText"].toObject()["simpleText"].toString(); ++ user = videoRenderer["ownerText"].toObject()["runs"].toArray().at(0).toObject()["text"].toString(); ++ publishedTime = videoRenderer["publishedTimeText"].toObject()["simpleText"].toString(); ++ viewCount = videoRenderer["shortViewCountText"].toObject()["simpleText"].toString(); ++ thumbnail = videoRenderer["thumbnail"].toObject()["thumbnails"].toArray().at(0).toObject()["url"].toString(); ++ url = YOUTUBE_URL "/watch?v=" + contentId; + } +- if ((idx = entry.indexOf("video-thumb")) > -1) +- { +- int skip = 0; +- int imgIdx = entry.indexOf("data-thumb=\"", idx); +- if (imgIdx > -1) +- skip = 12; +- else +- { +- imgIdx = entry.indexOf("src=\"", idx); +- skip = 5; +- } +- if (imgIdx > -1) +- { +- int imgEndIdx = entry.indexOf("\"", imgIdx += skip); +- if (imgEndIdx > -1) +- { +- image = entry.mid(imgIdx, imgEndIdx - imgIdx); +- if (image.endsWith(".gif")) //GIF nie jest miniaturką - jest to pojedynczy piksel :D (very old code, is it still relevant?) +- image.clear(); +- else if (image.startsWith("//")) +- image.prepend("https:"); +- if ((idx = image.indexOf("?")) > 0) +- image.truncate(idx); +- } +- } +- } +- if (!isPlaylist && (idx = entry.indexOf("video-time")) > -1 && (idx = entry.indexOf(">", idx)) > -1) +- { +- int endIdx = entry.indexOf("<", idx += 1); +- if (endIdx > -1) +- { +- duration = entry.mid(idx, endIdx - idx); +- if (!duration.startsWith("0") && duration.indexOf(":") == 1 && duration.count(":") == 1) +- duration.prepend("0"); +- } +- } +- if ((idx = entry.indexOf("yt-lockup-byline")) > -1) ++ else + { +- int endIdx = entry.indexOf("", idx); +- if (endIdx > -1 && (idx = entry.lastIndexOf(">", endIdx)) > -1) +- { +- ++idx; +- user = entry.mid(idx, endIdx - idx); +- } ++ title = playlistRenderer["title"].toObject()["simpleText"].toString(); ++ contentId = playlistRenderer["playlistId"].toString(); ++ if (title.isEmpty() || contentId.isEmpty()) ++ continue; ++ ++ user = playlistRenderer["longBylineText"].toObject()["runs"].toArray().at(0).toObject()["text"].toString(); ++ thumbnail = playlistRenderer ++ ["thumbnailRenderer"].toObject() ++ ["playlistVideoThumbnailRenderer"].toObject() ++ ["thumbnail"].toObject() ++ ["thumbnails"].toArray().at(0).toObject() ++ ["url"].toString() ++ ; ++ ++ url = YOUTUBE_URL "/playlist?list=" + contentId; + } + +- if (!title.isEmpty() && !videoInfoLink.isEmpty()) +- { +- QTreeWidgetItem *tWI = new QTreeWidgetItem(resultsW); +- tWI->setDisabled(true); ++ auto tWI = new QTreeWidgetItem(resultsW); + +- QTextDocument txtDoc; +- txtDoc.setHtml(title); ++ tWI->setText(0, title); ++ tWI->setText(1, isVideo ? length : tr("Playlist")); ++ tWI->setText(2, user); + +- tWI->setText(0, txtDoc.toPlainText()); +- tWI->setText(1, !isPlaylist ? duration : tr("Playlist")); +- tWI->setText(2, user); ++ QString tooltip; ++ tooltip += QString("%1: %2\n").arg(resultsW->headerItem()->text(0), tWI->text(0)); ++ tooltip += QString("%1: %2\n").arg(isVideo ? resultsW->headerItem()->text(1) : tr("Playlist"), isVideo ? tWI->text(1) : tr("yes")); ++ tooltip += QString("%1: %2\n").arg(resultsW->headerItem()->text(2), tWI->text(2)); ++ tooltip += QString("%1: %2\n").arg(tr("Published time"), publishedTime); ++ tooltip += QString("%1: %2").arg(tr("View count"), viewCount); ++ tWI->setToolTip(0, tooltip); + +- tWI->setToolTip(0, QString("%1: %2\n%3: %4\n%5: %6") +- .arg(resultsW->headerItem()->text(0), tWI->text(0), +- !isPlaylist ? resultsW->headerItem()->text(1) : tr("Playlist"), +- !isPlaylist ? tWI->text(1) : tr("yes"), +- resultsW->headerItem()->text(2), tWI->text(2)) +- ); ++ tWI->setData(0, Qt::UserRole, url); ++ tWI->setData(1, Qt::UserRole, !isVideo); + +- tWI->setData(0, Qt::UserRole, videoInfoLink); +- tWI->setData(1, Qt::UserRole, isPlaylist); ++ if (!isVideo) ++ { ++ tWI->setDisabled(true); + +- NetworkReply *linkReply = net.start(videoInfoLink); +- NetworkReply *imageReply = net.start(image); +- linkReply->setProperty("tWI", qVariantFromValue((void *)tWI)); +- imageReply->setProperty("tWI", qVariantFromValue((void *)tWI)); ++ auto linkReply = net.start(url); ++ linkReply->setProperty("tWI", QVariant::fromValue((void *)tWI)); + linkReplies += linkReply; ++ } ++ ++ if (!thumbnail.isEmpty()) ++ { ++ auto imageReply = net.start(thumbnail); ++ imageReply->setProperty("tWI", QVariant::fromValue((void *)tWI)); + imageReplies += imageReply; + } + } + +- if (i == 1) +- resultsW->clear(); +- else ++ if (resultsW->topLevelItemCount() > 0) + { + pageSwitcher->currPageB->setValue(currPage); + pageSwitcher->show(); +@@ -1249,44 +1213,53 @@ QStringList YouTube::getUrlByItagPriority(const QList &itags, QStringList r + return ret; + } + +-void YouTube::preparePlaylist(const QString &data, QTreeWidgetItem *tWI) ++void YouTube::preparePlaylist(const QByteArray &data, QTreeWidgetItem *tWI) + { +- int idx = data.indexOf("playlist-videos-container"); +- if (idx > -1) ++ QStringList playlist; ++ const auto json = getYtInitialData(data); ++ const auto contents = json.object() ++ ["contents"].toObject() ++ ["twoColumnBrowseResultsRenderer"].toObject() ++ ["tabs"].toArray().at(0).toObject() ++ ["tabRenderer"].toObject() ++ ["content"].toObject() ++ ["sectionListRenderer"].toObject() ++ ["contents"].toArray().at(0).toObject() ++ ["itemSectionRenderer"].toObject() ++ ["contents"].toArray().at(0).toObject() ++ ["playlistVideoListRenderer"].toObject() ++ ["contents"].toArray() ++ ; ++ for (auto &&obj : contents) + { +- const QString tags[2] = {"video-id", "video-title"}; +- QStringList playlist, entries = data.mid(idx).split("yt-uix-scroller-scroll-unit", QString::SkipEmptyParts); +- entries.removeFirst(); +- for (const QString &entry : entries) +- { +- QStringList plistEntry; +- for (int i = 0; i < 2; ++i) +- { +- idx = entry.indexOf(tags[i]); +- if (idx > -1 && (idx = entry.indexOf('"', idx += tags[i].length())) > -1) +- { +- const int endIdx = entry.indexOf('"', idx += 1); +- if (endIdx > -1) +- { +- const QString str = entry.mid(idx, endIdx - idx); +- if (!i) +- plistEntry += str; +- else +- { +- QTextDocument txtDoc; +- txtDoc.setHtml(str); +- plistEntry += txtDoc.toPlainText(); +- } +- } +- } +- } +- if (plistEntry.count() == 2) +- playlist += plistEntry; +- } +- if (!playlist.isEmpty()) +- { +- tWI->setData(0, Qt::UserRole + 1, playlist); +- tWI->setDisabled(false); +- } ++ const auto playlistRenderer = obj.toObject()["playlistVideoRenderer"].toObject(); ++ const auto title = playlistRenderer["title"].toObject()["simpleText"].toString(); ++ const auto videoId = playlistRenderer["videoId"].toString(); ++ if (title.isEmpty() || videoId.isEmpty()) ++ continue; ++ playlist += { ++ videoId, ++ title, ++ }; ++ } ++ if (!playlist.isEmpty()) ++ { ++ tWI->setData(0, Qt::UserRole + 1, playlist); ++ tWI->setDisabled(false); + } + } ++ ++QJsonDocument YouTube::getYtInitialData(const QByteArray &data) ++{ ++ int idx = data.indexOf("ytInitialData"); ++ if (idx < 0) ++ return QJsonDocument(); ++ idx = data.indexOf("{", idx); ++ if (idx < 0) ++ return QJsonDocument(); ++ int idx2 = Functions::findJsonEnd(data, idx); ++ if (idx2 < 0) ++ return QJsonDocument(); ++ const auto jsonData = data.mid(idx, idx2 - idx); ++ return QJsonDocument::fromJson(jsonData); ++} +diff --git src/modules/Extensions/YouTube.hpp src/modules/Extensions/YouTube.hpp +index 97e7be21..9070b408 100644 +--- src/modules/Extensions/YouTube.hpp ++++ src/modules/Extensions/YouTube.hpp +@@ -25,6 +25,7 @@ + #include + #include + #include ++#include + + class QProgressBar; + class QToolButton; +@@ -132,13 +133,15 @@ private: + void deleteReplies(); + + void setAutocomplete(const QByteArray &data); +- void setSearchResults(QString data); ++ void setSearchResults(const QByteArray &data); + + QStringList getYouTubeVideo(const QString &data, const QString &PARAM = QString(), QTreeWidgetItem *tWI = nullptr, const QString &url = QString(), IOController *youtube_dl = nullptr); //jeżeli (tWI == nullptr) to zwraca {URL, file_extension, TITLE} + QStringList getUrlByItagPriority(const QList &itags, QStringList ret); + +- void preparePlaylist(const QString &data, QTreeWidgetItem *tWI); ++ void preparePlaylist(const QByteArray &data, QTreeWidgetItem *tWI); + ++ QJsonDocument getYtInitialData(const QByteArray &data); ++private: + DockWidget *dw; + + QIcon youtubeIcon, videoIcon; +diff --git src/qmplay2/Functions.cpp src/qmplay2/Functions.cpp +index 35e59931..03ec70ee 100644 +--- src/qmplay2/Functions.cpp ++++ src/qmplay2/Functions.cpp +@@ -912,3 +912,34 @@ QByteArray Functions::decryptAes256Cbc(const QByteArray &password, const QByteAr + deciphered.resize(decryptedLen + finalizeLen); + return deciphered; + } ++ ++int Functions::findJsonEnd(const QByteArray &data, int idx) ++{ ++ const int dataLen = data.length(); ++ if (dataLen < 1 || idx < 0 || idx >= dataLen || data.at(idx) != '{') ++ return -1; ++ int brackets = 1; ++ bool inString = false; ++ char prevChr = '\0'; ++ for (int i = idx + 1; i < dataLen; ++i) ++ { ++ const char chr = data.at(i); ++ if (chr == '"') ++ { ++ if (!inString) ++ inString = true; ++ else if (prevChr != '\\') ++ inString = false; ++ } ++ prevChr = chr; ++ if (inString) ++ continue; ++ if (chr == '{') ++ ++brackets; ++ else if (chr == '}') ++ --brackets; ++ if (brackets == 0) ++ return i + 1; ++ } ++ return -1; ++} +diff --git src/qmplay2/headers/Functions.hpp src/qmplay2/headers/Functions.hpp +index e74c48f1..ef4dd090 100644 +--- src/qmplay2/headers/Functions.hpp ++++ src/qmplay2/headers/Functions.hpp +@@ -158,4 +158,6 @@ namespace Functions + void setHeaderSectionResizeMode(QHeaderView *header, int index, int resizeMode); + + QByteArray decryptAes256Cbc(const QByteArray &password, const QByteArray &salt, const QByteArray &ciphered); ++ ++ int findJsonEnd(const QByteArray &data, int idx = 0); + } diff --git a/multimedia/QMPlay2/files/0008-Switch-to-yt-dlp.patch b/multimedia/QMPlay2/files/0008-Switch-to-yt-dlp.patch new file mode 100644 index 0000000000000..fc70aafd8c1fa --- /dev/null +++ b/multimedia/QMPlay2/files/0008-Switch-to-yt-dlp.patch @@ -0,0 +1,85 @@ +From bf4f953cf6f083d6bc876543f7ac8e77a123abbf Mon Sep 17 00:00:00 2001 +From: Sergey Fedorov +Date: Tue, 19 Nov 2024 17:04:56 +0800 +Subject: [PATCH 08/11] Switch to yt-dlp + +--- + src/qmplay2/YouTubeDL.cpp | 40 ++++++++++----------------------------- + 1 file changed, 10 insertions(+), 30 deletions(-) + +diff --git src/qmplay2/YouTubeDL.cpp src/qmplay2/YouTubeDL.cpp +index de7b96be..98071fea 100644 +--- src/qmplay2/YouTubeDL.cpp ++++ src/qmplay2/YouTubeDL.cpp +@@ -22,10 +22,9 @@ + #include + #include + #include +-#ifdef Q_OS_WIN +- #include +-#endif ++#include + ++#include + #include + #include + +@@ -50,9 +49,9 @@ static void exportCookiesFromJSON(const QString &jsonData, const QString &url) + + QString YouTubeDL::getFilePath() + { +- return QMPlay2Core.getSettingsDir() + "youtube-dl" ++ return QMPlay2Core.getSettingsDir() + "yt-dlp" + #ifdef Q_OS_WIN +- ".exe" ++ "_x86.exe" + #endif + ; + } +@@ -286,30 +285,11 @@ QStringList YouTubeDL::exec(const QString &url, const QStringList &args, QString + updateOutput += m_process.readAllStandardOutput() + m_process.readAllStandardError(); + if (updateOutput.contains("ERROR:") || updateOutput.contains("package manager")) + error += "\n" + updateOutput; +- else if (m_process.exitCode() == 0 && !updateOutput.contains("up-to-date")) ++ else if (m_process.exitCode() == 0 && !updateOutput.contains(QRegExp(R"(up\Wto\Wdate)"))) + { +-#ifdef Q_OS_WIN +- const QString updatedFile = ytDlPath + ".new"; +- QFile::remove(Functions::filePath(ytDlPath) + "youtube-dl-updater.bat"); +- if (QFile::exists(updatedFile)) +- { +- Functions::s_wait(0.2); //Wait 200 ms to be sure that file is closed +- QFile::remove(ytDlPath); +- if (QFile::rename(updatedFile, ytDlPath)) +-#endif +- { +- QMPlay2Core.setWorking(false); +- emit QMPlay2Core.sendMessage(tr("\"youtube-dl\" has been successfully updated!"), g_name); +- g_lock.unlock(); // Unlock for write +- return exec(url, args, silentErr, false); +- } +-#ifdef Q_OS_WIN +- } +- else +- { +- error += "\nUpdated youtube-dl file: \"" + updatedFile + "\" not found!"; +- } +-#endif ++ QMPlay2Core.setWorking(false); ++ emit QMPlay2Core.sendMessage(tr("\"youtube-dl\" has been successfully updated!"), g_name); ++ return {}; + } + } + else if (updating && m_aborted) +@@ -339,9 +319,9 @@ QStringList YouTubeDL::exec(const QString &url, const QStringList &args, QString + } + else if (canUpdate && !m_aborted && m_process.error() == QProcess::FailedToStart) + { +- const QString downloadUrl = "https://yt-dl.org/downloads/latest/youtube-dl" ++ const QString downloadUrl = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp" + #ifdef Q_OS_WIN +- ".exe" ++ "_x86.exe" + #endif + ; + diff --git a/multimedia/QMPlay2/files/0009-PortAudio-Allow-bit-perfect-audio-output-for-macOS-1.patch b/multimedia/QMPlay2/files/0009-PortAudio-Allow-bit-perfect-audio-output-for-macOS-1.patch new file mode 100644 index 0000000000000..6ec8eb1db4d4d --- /dev/null +++ b/multimedia/QMPlay2/files/0009-PortAudio-Allow-bit-perfect-audio-output-for-macOS-1.patch @@ -0,0 +1,1207 @@ +From 4ce1e7e12d4801d03c25ae6b9ef12ef366c0b083 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ren=C3=A9=20Bertin?= +Date: Mon, 12 Feb 2018 18:20:07 +0100 +Subject: [PATCH] PortAudio: Allow bit-perfect audio output for macOS + (#108) + +When QMPlay2 isn't configured to resample all audio content to a +user-specific sample rate it sends the demuxed and decoded audio stream +"as is" to the selected audio device. When using the PortAudio module a +stream is opened that is set to the content sample rate, but no attempt +is made to configure the device appropriately. +This patch introduces an initial implementation of a bit-perfect audio +option in the PortAudio module for macOS. After opening the PA stream, +platform-native APIs are called to switch the selected audio device to +the content's sample rate or to the most appropriate (closest) supported +sample rate. The initial device settings are cached and restored when +playback is stopped. + +barracuda156: Some fixes added for Qt4 compat. +--- + .../3rdparty/CoreAudio/AudioDevice.h | 136 ++++ + .../3rdparty/CoreAudio/AudioDevice.mm | 645 ++++++++++++++++++ + .../3rdparty/CoreAudio/AudioDeviceList.cpp | 98 +++ + .../3rdparty/CoreAudio/AudioDeviceList.h | 82 +++ + src/modules/PortAudio/CMakeLists.txt | 10 + + src/modules/PortAudio/PortAudio.cpp | 12 + + src/modules/PortAudio/PortAudio.hpp | 3 + + src/modules/PortAudio/PortAudioWriter.cpp | 53 +- + src/modules/PortAudio/PortAudioWriter.hpp | 7 + + 9 files changed, 1045 insertions(+), 1 deletion(-) + create mode 100644 src/modules/PortAudio/3rdparty/CoreAudio/AudioDevice.h + create mode 100644 src/modules/PortAudio/3rdparty/CoreAudio/AudioDevice.mm + create mode 100644 src/modules/PortAudio/3rdparty/CoreAudio/AudioDeviceList.cpp + create mode 100644 src/modules/PortAudio/3rdparty/CoreAudio/AudioDeviceList.h + +diff --git src/modules/PortAudio/3rdparty/CoreAudio/AudioDevice.h src/modules/PortAudio/3rdparty/CoreAudio/AudioDevice.h +new file mode 100644 +index 00000000..9d5788e4 +--- /dev/null ++++ src/modules/PortAudio/3rdparty/CoreAudio/AudioDevice.h +@@ -0,0 +1,136 @@ ++/* ++ File: AudioDevice.h ++ Adapted from the CAPlayThough example ++ Version: 1.2.2 ++ ++ Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple ++ Inc. ("Apple") in consideration of your agreement to the following ++ terms, and your use, installation, modification or redistribution of ++ this Apple software constitutes acceptance of these terms. If you do ++ not agree with these terms, please do not use, install, modify or ++ redistribute this Apple software. ++ ++ In consideration of your agreement to abide by the following terms, and ++ subject to these terms, Apple grants you a personal, non-exclusive ++ license, under Apple's copyrights in this original Apple software (the ++ "Apple Software"), to use, reproduce, modify and redistribute the Apple ++ Software, with or without modifications, in source and/or binary forms; ++ provided that if you redistribute the Apple Software in its entirety and ++ without modifications, you must retain this notice and the following ++ text and disclaimers in all such redistributions of the Apple Software. ++ Neither the name, trademarks, service marks or logos of Apple Inc. may ++ be used to endorse or promote products derived from the Apple Software ++ without specific prior written permission from Apple. Except as ++ expressly stated in this notice, no other rights or licenses, express or ++ implied, are granted by Apple herein, including but not limited to any ++ patent rights that may be infringed by your derivative works or by other ++ works in which the Apple Software may be incorporated. ++ ++ The Apple Software is provided by Apple on an "AS IS" basis. APPLE ++ MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ++ THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS ++ FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND ++ OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. ++ ++ IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL ++ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, ++ MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED ++ AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), ++ STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE ++ POSSIBILITY OF SUCH DAMAGE. ++ ++ Copyright (C) 2013 Apple Inc. All Rights Reserved. ++ Copyright (C) 2017,18 René J.V. Bertin All Rights Reserved. ++ ++*/ ++ ++#ifndef __AudioDevice_h__ ++#define __AudioDevice_h__ ++ ++#include ++#include ++#include ++ ++#include ++ ++#ifndef DEPRECATED_LISTENER_API ++# if MAC_OS_X_VERSION_MIN_REQUIRED < 101100 ++# define DEPRECATED_LISTENER_API ++# warning "Using the deprecated PropertyListener API; at least it works" ++# endif ++#endif ++ ++#ifdef DEPRECATED_LISTENER_API ++using AudioPropertyListenerProc = AudioDevicePropertyListenerProc; ++#else ++using AudioPropertyListenerProc = AudioObjectPropertyListenerProc; ++#endif ++ ++class AudioDeviceList; ++ ++class AudioDevice { ++public: ++ AudioDevice(); ++ AudioDevice(AudioDeviceID devid, bool isInput=false); ++ AudioDevice(AudioDeviceID devid, AudioPropertyListenerProc lProc, bool isInput=false); ++ ~AudioDevice(); ++ ++ void Init(AudioPropertyListenerProc lProc); ++ ++ bool Valid() { return mID != kAudioDeviceUnknown; } ++ ++ void SetBufferSize(UInt32 size); ++ OSStatus NominalSampleRate(Float64 &sampleRate); ++ inline Float64 ClosestNominalSampleRate(Float64 sampleRate); ++ OSStatus SetNominalSampleRate(Float64 sampleRate, Boolean force=false); ++ OSStatus ResetNominalSampleRate(Boolean force=false); ++ OSStatus SetStreamBasicDescription(AudioStreamBasicDescription *desc); ++ int CountChannels(); ++ char *GetName(char *buf=NULL, UInt32 maxlen=0); ++ ++ void SetInitialNominalSampleRate(Float64 sampleRate) ++ { ++ mInitialFormat.mSampleRate = sampleRate; ++ } ++ ++ Float64 CurrentNominalSampleRate() ++ { ++ return currentNominalSR; ++ } ++ ++ AudioDeviceID ID() ++ { ++ return mID; ++ } ++ ++ static AudioDevice *GetDefaultDevice(Boolean forInput, OSStatus &err, AudioDevice *dev=NULL); ++ static AudioDevice *GetDevice(AudioDeviceID devId, Boolean forInput, AudioDevice *dev=NULL); ++ ++protected: ++ AudioDevice(AudioDeviceID devid, bool quick, bool isInput); ++ AudioStreamBasicDescription mInitialFormat; ++ AudioPropertyListenerProc listenerProc; ++ OSStatus GetPropertyDataSize( AudioObjectPropertySelector property, UInt32 *size, AudioObjectPropertyAddress *propertyAddress=NULL ); ++ Float64 currentNominalSR; ++ Float64 minNominalSR, maxNominalSR; ++ UInt32 nominalSampleRates; ++ Float64 *nominalSampleRateList = NULL; ++ bool discreteSampleRateList; ++ const AudioDeviceID mID; ++ const bool mForInput; ++ UInt32 mSafetyOffset; ++ UInt32 mBufferSizeFrames; ++ AudioStreamBasicDescription mFormat; ++ char mDevName[256] = ""; ++ ++ bool mInitialised = false; ++ ++friend class AudioDeviceList; ++ ++public: ++ UInt32 listenerSilentFor; ++}; ++ ++#endif // __AudioDevice_h__ +diff --git src/modules/PortAudio/3rdparty/CoreAudio/AudioDevice.mm src/modules/PortAudio/3rdparty/CoreAudio/AudioDevice.mm +new file mode 100644 +index 00000000..f631b4e7 +--- /dev/null ++++ src/modules/PortAudio/3rdparty/CoreAudio/AudioDevice.mm +@@ -0,0 +1,645 @@ ++/* ++ File: AudioDevice.cpp ++ Adapted from the CAPlayThough example ++ Version: 1.2.2 ++ ++ Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple ++ Inc. ("Apple") in consideration of your agreement to the following ++ terms, and your use, installation, modification or redistribution of ++ this Apple software constitutes acceptance of these terms. If you do ++ not agree with these terms, please do not use, install, modify or ++ redistribute this Apple software. ++ ++ In consideration of your agreement to abide by the following terms, and ++ subject to these terms, Apple grants you a personal, non-exclusive ++ license, under Apple's copyrights in this original Apple software (the ++ "Apple Software"), to use, reproduce, modify and redistribute the Apple ++ Software, with or without modifications, in source and/or binary forms; ++ provided that if you redistribute the Apple Software in its entirety and ++ without modifications, you must retain this notice and the following ++ text and disclaimers in all such redistributions of the Apple Software. ++ Neither the name, trademarks, service marks or logos of Apple Inc. may ++ be used to endorse or promote products derived from the Apple Software ++ without specific prior written permission from Apple. Except as ++ expressly stated in this notice, no other rights or licenses, express or ++ implied, are granted by Apple herein, including but not limited to any ++ patent rights that may be infringed by your derivative works or by other ++ works in which the Apple Software may be incorporated. ++ ++ The Apple Software is provided by Apple on an "AS IS" basis. APPLE ++ MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ++ THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS ++ FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND ++ OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. ++ ++ IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL ++ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, ++ MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED ++ AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), ++ STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE ++ POSSIBILITY OF SUCH DAMAGE. ++ ++ Copyright (C) 2013 Apple Inc. All Rights Reserved. ++ Copyright (C) 2017,18 René J.V. Bertin All Rights Reserved. ++ ++*/ ++ ++#include "AudioDevice.h" ++#import ++ ++#if MAC_OS_X_VERSION_MIN_REQUIRED > 1080 ++ #define verify_noerr __Verify_noErr ++#endif ++ ++char *OSTStr(OSType type) ++{ ++ static union OSTStr { ++ uint32_t four; ++ char str[5]; ++ } ltype; ++ ltype.four = EndianU32_BtoN(type); ++ ltype.str[4] = '\0'; ++ return ltype.str; ++} ++ ++// the sample rates that can be found in the selections proposed by Audio Midi Setup. Are these representative ++// for the devices I have at my disposal, or are they determined by discrete supported values hardcoded into ++// CoreAudio or the HAL? Is there any advantage in using one of these rates, as opposed to using a different rate ++// on devices that support any rate in an interval? ++static Float64 supportedSRateList[] = {6400, 8000, 11025, 12000, 16000, 22050, ++ 24000, 32000, 44100, 48000, 64000, 88200, 96000, 128000, 176400, 192000 ++ }; ++static UInt32 supportedSRates = sizeof(supportedSRateList) / sizeof(Float64); ++ ++#ifdef DEPRECATED_LISTENER_API ++ ++OSStatus DefaultListener(AudioDeviceID inDevice, UInt32 inChannel, Boolean forInput, ++ AudioDevicePropertyID inPropertyID, ++ void *inClientData) ++{ ++ UInt32 size; ++ Float64 sampleRate; ++ AudioDevice *dev = (AudioDevice *) inClientData; ++ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; ++ NSString *msg = [NSString stringWithFormat:@"Property %s of device %u changed; data=%p", ++ OSTStr((OSType)inPropertyID), (unsigned int)inDevice, inClientData ]; ++ AudioObjectPropertyAddress theAddress = { inPropertyID, ++ forInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, ++ kAudioObjectPropertyElementMaster ++ }; ++ ++ switch (inPropertyID) { ++ case kAudioDevicePropertyNominalSampleRate: ++ size = sizeof(sampleRate); ++ if (AudioObjectGetPropertyData(inPropertyID, &theAddress, 0, NULL, &size, &sampleRate) == noErr ++ && (dev && !dev->listenerSilentFor)) { ++ NSLog(@"%@\n\tkAudioDevicePropertyNominalSampleRate=%g\n", msg, sampleRate); ++ } ++ break; ++ case kAudioDevicePropertyActualSampleRate: ++ size = sizeof(sampleRate); ++ if (AudioObjectGetPropertyData(inPropertyID, &theAddress, 0, NULL, &size, &sampleRate) == noErr && dev) { ++ // update the rate we should reset to ++ dev->SetInitialNominalSampleRate(sampleRate); ++ if (!dev->listenerSilentFor) { ++ NSLog(@"%@\n\tkAudioDevicePropertyActualSampleRate=%g\n", msg, sampleRate); ++ } ++ } ++ break; ++ default: ++ if ((dev && !dev->listenerSilentFor)) { ++ NSLog(@"%@", msg); ++ } ++ break; ++ } ++ if (dev && dev->listenerSilentFor) { ++ dev->listenerSilentFor -= 1; ++ } ++ if (pool) { ++ [pool drain]; ++ } ++ return noErr; ++} ++ ++#else // !DEPRECATED_LISTENER_API ++ ++static OSStatus DefaultListener(AudioObjectID inObjectID, UInt32 inNumberProperties, ++ const AudioObjectPropertyAddress propTable[], ++ void *inClientData) ++{ ++ UInt32 size; ++ Float64 sampleRate; ++ AudioDevice *dev = static_cast(inClientData); ++ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; ++ for (int i = 0 ; i < inNumberProperties ; ++i) { ++ NSString *msg = [NSString stringWithFormat:@"#%d Property %s of device %u changed; data=%p", i, ++ OSTStr((OSType)propTable[i].mElement), (unsigned int)inObjectID, inClientData ]; ++ switch (propTable[i].mElement) { ++ case kAudioDevicePropertyNominalSampleRate: ++ size = sizeof(sampleRate); ++ if (AudioObjectGetPropertyData(inObjectID, &propTable[i], 0, NULL, &size, &sampleRate) == noErr ++ && (dev && !dev->listenerSilentFor) ++ ) { ++ NSLog(@"%@\n\tkAudioDevicePropertyNominalSampleRate=%g\n", msg, sampleRate); ++ } ++ break; ++ case kAudioDevicePropertyActualSampleRate: ++ size = sizeof(sampleRate); ++ if (AudioObjectGetPropertyData(inObjectID, &propTable[i], 0, NULL, &size, &sampleRate) == noErr && dev) { ++ // update the rate we should reset to ++ dev->SetInitialNominalSampleRate(sampleRate); ++ if (!dev->listenerSilentFor) { ++ NSLog(@"%@\n\tkAudioDevicePropertyActualSampleRate=%g\n", msg, sampleRate); ++ } ++ } ++ break; ++ default: ++ if ((dev && !dev->listenerSilentFor)) { ++ NSLog(@"%@", msg); ++ } ++ break; ++ } ++ } ++ if (dev && dev->listenerSilentFor) { ++ dev->listenerSilentFor -= 1; ++ } ++ if (pool) { ++ [pool drain]; ++ } ++ return noErr; ++} ++#endif ++ ++void AudioDevice::Init(AudioPropertyListenerProc lProc = DefaultListener) ++{ ++ if (mID == kAudioDeviceUnknown) { ++ return; ++ } ++ OSStatus err = noErr; ++ ++ // getting the device name can be surprisingly slow, so we get and cache it here ++ GetName(); ++ ++ UInt32 propsize = sizeof(Float32); ++ ++ AudioObjectPropertyAddress theAddress = { kAudioDevicePropertySafetyOffset, ++ mForInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, ++ kAudioObjectPropertyElementMaster ++ }; // channel ++ verify_noerr(AudioObjectGetPropertyData(mID, &theAddress, 0, NULL, &propsize, &mSafetyOffset)); ++ ++ propsize = sizeof(UInt32); ++ theAddress.mSelector = kAudioDevicePropertyBufferFrameSize; ++ verify_noerr(AudioObjectGetPropertyData(mID, &theAddress, 0, NULL, &propsize, &mBufferSizeFrames)); ++ ++ listenerProc = lProc; ++ listenerSilentFor = 0; ++ if (lProc) { ++#ifdef DEPRECATED_LISTENER_API ++ AudioDeviceAddPropertyListener(mID, 0, false, kAudioDevicePropertyActualSampleRate, lProc, this); ++ AudioDeviceAddPropertyListener(mID, 0, false, kAudioDevicePropertyNominalSampleRate, lProc, this); ++ AudioDeviceAddPropertyListener(mID, 0, false, kAudioHardwarePropertyDefaultOutputDevice, lProc, this); ++#else ++ AudioObjectPropertyAddress prop = { kAudioDevicePropertyActualSampleRate, ++ kAudioObjectPropertyScopeGlobal, ++ kAudioObjectPropertyElementMaster ++ }; ++ if ((err = AudioObjectAddPropertyListener(mID, &prop, lProc, this)) != noErr) { ++ NSLog(@"Couldn't register property listener for actual sample rate: %d (%s)", err, OSTStr(err)); ++ } ++ prop.mElement = kAudioDevicePropertyNominalSampleRate; ++ if ((err = AudioObjectAddPropertyListener(mID, &prop, lProc, this)) != noErr) { ++ NSLog(@"Couldn't register property listener for nominal sample rate: %d (%s)", err, OSTStr(err)); ++ } ++ prop.mElement = kAudioHardwarePropertyDefaultOutputDevice; ++ if ((err = AudioObjectAddPropertyListener(mID, &prop, lProc, this)) != noErr) { ++ NSLog(@"Couldn't register property listener for selected default device: %d (%s)", err, OSTStr(err)); ++ } ++#endif ++ } else { ++ NSLog(@"Warning: no CoreAudio event listener has been defined"); ++ } ++ propsize = sizeof(Float64); ++ theAddress.mSelector = kAudioDevicePropertyNominalSampleRate; ++ verify_noerr(AudioObjectGetPropertyData(mID, &theAddress, 0, NULL, &propsize, ¤tNominalSR)); ++ propsize = sizeof(AudioStreamBasicDescription); ++ theAddress.mSelector = kAudioDevicePropertyStreamFormat; ++ verify_noerr(AudioObjectGetPropertyData(mID, &theAddress, 0, NULL, &propsize, &mInitialFormat)); ++ mFormat = mInitialFormat; ++ propsize = 0; ++ theAddress.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; ++ // attempt to build a list of the supported sample rates ++ if ((err = AudioObjectGetPropertyDataSize(mID, &theAddress, 0, NULL, &propsize)) == noErr) { ++ AudioValueRange *list; ++ // use a fall-back value of 100 supported rates: ++ if (propsize == 0) { ++ propsize = 100 * sizeof(AudioValueRange); ++ } ++ if ((list = (AudioValueRange *) calloc(1, propsize))) { ++ theAddress.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; ++ err = AudioObjectGetPropertyData(mID, &theAddress, 0, NULL, &propsize, list); ++ if (err == noErr) { ++ UInt32 i; ++ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; ++ nominalSampleRates = propsize / sizeof(AudioValueRange); ++ NSMutableArray *a = [NSMutableArray arrayWithCapacity:nominalSampleRates]; ++ minNominalSR = list[0].mMinimum; ++ maxNominalSR = list[0].mMaximum; ++ // store the returned sample rates in [a] and record the extreme values ++ for (i = 0 ; i < nominalSampleRates ; i++) { ++ if (minNominalSR > list[i].mMinimum) { ++ minNominalSR = list[i].mMinimum; ++ } ++ if (maxNominalSR < list[i].mMaximum) { ++ maxNominalSR = list[i].mMaximum; ++ } ++ if (a) { ++ if (list[i].mMinimum != list[i].mMaximum) { ++ UInt32 j; ++ discreteSampleRateList = false; ++ // the 'guessing' case: the device specifies one or more ranges, without ++ // indicating which rates in that range(s) are supported. We assume the ++ // rates that Audio Midi Setup shows. ++ for (j = 0 ; j < supportedSRates ; j++) { ++ if (supportedSRateList[j] >= list[i].mMinimum ++ && supportedSRateList[j] <= list[i].mMaximum ++ ) { ++ [a addObject:[NSNumber numberWithDouble:supportedSRateList[j]]]; ++ } ++ } ++ } else { ++ // there's at least one part of the sample rate list that contains discrete ++ // supported values. I don't know if there are devices that "do this" or if they all ++ // either give discrete rates or a single continuous range. So we take the easy ++ // opt-out solution: this only costs a few cycles attempting to match a requested ++ // non-listed rate (with the potential "risk" of matching to a listed integer multiple, ++ // which should not cause any aliasing). ++ discreteSampleRateList = true; ++ // the easy case: the device specifies one or more discrete rates ++ if (![a containsObject:[NSNumber numberWithDouble:list[i].mMinimum]]) { ++ [a addObject:[NSNumber numberWithDouble:list[i].mMinimum]]; ++ } ++ } ++ } ++ } ++ if (a) { ++ // sort the array (should be the case but one never knows) ++ [a sortUsingSelector:@selector(compare:)]; ++ // retrieve the number of unique rates: ++ nominalSampleRates = [a count]; ++ // now copy the rates into a simple C array for faster access ++ if ((nominalSampleRateList = new Float64[nominalSampleRates])) { ++ for (i = 0 ; i < nominalSampleRates ; i++) { ++ nominalSampleRateList[i] = [[a objectAtIndex:i] doubleValue]; ++ } ++ } ++ NSLog(@"Using audio device %u \"%s\", %u sample rates in %lu range(s); [%g,%g] %@; current sample rate %gHz", ++ mID, GetName(), nominalSampleRates, propsize / sizeof(AudioValueRange), ++ minNominalSR, maxNominalSR, (discreteSampleRateList) ? [a description] : @"continuous", currentNominalSR); ++ } else { ++ NSLog(@"Using audio device %u \"%s\", %u sample rates in %lu range(s); [%g,%g] %s; current sample rate %gHz", ++ mID, GetName(), nominalSampleRates, propsize / sizeof(AudioValueRange), ++ minNominalSR, maxNominalSR, (discreteSampleRateList) ? "" : "continuous", currentNominalSR); ++ } ++ // [a] will be flushed down the drain: ++ [pool drain]; ++ } ++ free(list); ++ } ++ mInitialised = true; ++ } ++} ++ ++AudioDevice::AudioDevice() ++ : mID(kAudioDeviceUnknown) ++ , mForInput(false) ++{ ++ listenerProc = NULL; ++} ++ ++AudioDevice::AudioDevice(AudioDeviceID devid, bool forInput) ++ : mID(devid) ++ , mForInput(forInput) ++{ ++ Init(DefaultListener); ++} ++ ++AudioDevice::AudioDevice(AudioDeviceID devid, bool quick, bool forInput) ++ : mID(devid) ++ , mForInput(forInput) ++{ ++ if (!quick) { ++ Init(DefaultListener); ++ } ++} ++ ++AudioDevice::AudioDevice(AudioDeviceID devid, AudioPropertyListenerProc lProc, bool forInput) ++ : mID(devid) ++ , mForInput(forInput) ++{ ++ Init(lProc); ++} ++ ++AudioDevice::~AudioDevice() ++{ ++ if (mID != kAudioDeviceUnknown && mInitialised) { ++ OSStatus err; ++ AudioDeviceID devId = mID; ++ // RJVB 20120902: setting the StreamFormat to the initially read values will set the channel bitdepth to 16?? ++ // so we reset just the nominal sample rate. ++ err = SetNominalSampleRate(mInitialFormat.mSampleRate); ++ if (err != noErr) { ++ fprintf(stderr, "Cannot reset initial settings for device %u (%s): err %s, %ld\n", ++ (unsigned int) mID, GetName(), OSTStr(err), (long) err); ++ } ++ if (listenerProc) { ++#ifdef DEPRECATED_LISTENER_API ++ AudioDeviceRemovePropertyListener(mID, 0, false, kAudioDevicePropertyActualSampleRate, listenerProc); ++ AudioDeviceRemovePropertyListener(mID, 0, false, kAudioDevicePropertyNominalSampleRate, listenerProc); ++ AudioDeviceRemovePropertyListener(mID, 0, false, kAudioHardwarePropertyDefaultOutputDevice, listenerProc); ++#else ++ AudioObjectPropertyAddress prop = { kAudioDevicePropertyActualSampleRate, ++ kAudioObjectPropertyScopeGlobal, ++ kAudioObjectPropertyElementMaster ++ }; ++ verify_noerr(AudioObjectRemovePropertyListener(mID, &prop, listenerProc, this)); ++ prop.mElement = kAudioDevicePropertyNominalSampleRate; ++ verify_noerr(AudioObjectRemovePropertyListener(mID, &prop, listenerProc, this)); ++ prop.mElement = kAudioHardwarePropertyDefaultOutputDevice; ++ verify_noerr(AudioObjectRemovePropertyListener(mID, &prop, listenerProc, this)); ++#endif ++ } ++ if (nominalSampleRateList) { ++ delete nominalSampleRateList; ++ } ++ NSLog(@"AudioDevice %s (%u) released", mDevName, devId); ++ } ++} ++ ++void AudioDevice::SetBufferSize(UInt32 size) ++{ ++ UInt32 propsize = sizeof(UInt32); ++ AudioObjectPropertyAddress theAddress = { kAudioDevicePropertyBufferFrameSize, ++ mForInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, ++ kAudioObjectPropertyElementMaster ++ }; // channel ++ ++ verify_noerr(AudioObjectSetPropertyData(mID, &theAddress, 0, NULL, propsize, &size)); ++ verify_noerr(AudioObjectGetPropertyData(mID, &theAddress, 0, NULL, &propsize, &mBufferSizeFrames)); ++} ++ ++OSStatus AudioDevice::NominalSampleRate(Float64 &sampleRate) ++{ ++ UInt32 size = sizeof(Float64); ++ OSStatus err; ++ AudioObjectPropertyAddress theAddress = { kAudioDevicePropertyNominalSampleRate, ++ mForInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, ++ kAudioObjectPropertyElementMaster ++ }; ++ err = AudioObjectGetPropertyData(mID, &theAddress, 0, NULL, &size, &sampleRate); ++ if (err == noErr) { ++ currentNominalSR = sampleRate; ++ } ++ return err; ++} ++ ++inline Float64 AudioDevice::ClosestNominalSampleRate(Float64 sampleRate) ++{ ++ if (sampleRate > 0) { ++#ifndef FORCE_STANDARD_SAMPLERATES ++ if (!discreteSampleRateList && sampleRate >= minNominalSR && sampleRate <= maxNominalSR) { ++ // the device suggests it supports this exact sample rate; use it. ++ listenerSilentFor = 0; ++ return sampleRate; ++ } ++#endif ++ if (nominalSampleRateList && sampleRate >= minNominalSR && sampleRate <= maxNominalSR) { ++ Float64 minRemainder = 1; ++ Float64 closest = 0; ++ for (UInt32 i = 0 ; i < nominalSampleRates ; i++) { ++ // check if we have a hit: ++ if (sampleRate == nominalSampleRateList[i]) { ++ return sampleRate; ++ } ++ double dec, ent; ++ dec = modf(nominalSampleRateList[i] / sampleRate, &ent); ++ // if the rate at i is an integer multiple of the requested sample rate. ++ if (dec == 0) { ++ listenerSilentFor = 0; ++ return nominalSampleRateList[i]; ++ } else if ((1 - dec) < minRemainder) { ++ // find the match that is closest to either 1*sampleRate or 2*sampleRate ++ // IOW, the fractional part of nominalSampleRate/sampleRate is closest ++ // either to 0 or to 1 . ++ minRemainder = dec; ++ closest = nominalSampleRateList[i]; ++ } ++ } ++ if (closest > 0) { ++ listenerSilentFor = 0; ++ return closest; ++ } ++ } ++ static bool pass2 = false; ++ if (!pass2) { ++ Float64 sr = sampleRate; ++ int fact = 1; ++ // if we're here it's either because there's no list of known supported rates, ++ // or we didn't find an integer multiple of the requested rate in the list. ++ // scale up as required in steps of 2 ++ while (sampleRate * fact < minNominalSR && sampleRate * (fact + 1) <= maxNominalSR) { ++ fact += 1; ++ } ++ sampleRate *= fact; ++ fact = 1; ++ // scale down as required in steps of 2 ++ while (sampleRate / fact > maxNominalSR && sampleRate / (fact + 1) >= minNominalSR) { ++ fact += 1; ++ } ++ sampleRate /= fact; ++ if (sr != sampleRate) { ++ pass2 = true; ++ // we're now in range, do another pass to find a matching supported rate ++ sampleRate = ClosestNominalSampleRate(sampleRate); ++ pass2 = false; ++ listenerSilentFor = 0; ++ } else { ++ if (sampleRate > maxNominalSR) { ++ sampleRate = maxNominalSR; ++ } else { ++ sampleRate = minNominalSR; ++ } ++ } ++ // note that we really ought to resample the content if we're sending it to a ++ // device running at a lower sample rate! ++ } ++ } ++ return sampleRate; ++} ++ ++OSStatus AudioDevice::SetNominalSampleRate(Float64 sampleRate, Boolean force) ++{ ++ UInt32 size = sizeof(Float64); ++ OSStatus err; ++ if (sampleRate <= 0) { ++ return paramErr; ++ } ++ listenerSilentFor = 2; ++ Float64 sampleRate2 = ClosestNominalSampleRate(sampleRate); ++ NSLog(@"SetNominalSampleRate(%g) setting rate to %gHz", sampleRate, sampleRate2); ++ if (sampleRate2 != currentNominalSR || force) { ++ AudioObjectPropertyAddress theAddress = { kAudioDevicePropertyNominalSampleRate, ++ mForInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, ++ kAudioObjectPropertyElementMaster ++ }; ++ err = AudioObjectSetPropertyData(mID, &theAddress, 0, NULL, size, &sampleRate2); ++ if (err == noErr) { ++ currentNominalSR = sampleRate2; ++ } else { ++ NSLog(@"Failure setting device \"%s\" to %gHz: %d (%s)", GetName(), sampleRate2, err, OSTStr(err)); ++ } ++ } else { ++ err = noErr; ++ } ++ return err; ++} ++ ++/*! ++ Reset the nominal sample rate to the value found when opening the device ++ */ ++OSStatus AudioDevice::ResetNominalSampleRate(Boolean force) ++{ ++ UInt32 size = sizeof(Float64); ++ Float64 sampleRate = mInitialFormat.mSampleRate; ++ OSStatus err = noErr; ++ if (sampleRate != currentNominalSR || force) { ++ listenerSilentFor = 2; ++ AudioObjectPropertyAddress theAddress = { kAudioDevicePropertyNominalSampleRate, ++ mForInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, ++ kAudioObjectPropertyElementMaster ++ }; ++ err = AudioObjectSetPropertyData(mID, &theAddress, 0, NULL, size, &sampleRate); ++ if (err == noErr) { ++ currentNominalSR = sampleRate; ++ } ++ } ++ return err; ++} ++ ++OSStatus AudioDevice::SetStreamBasicDescription(AudioStreamBasicDescription *desc) ++{ ++ UInt32 size = sizeof(AudioStreamBasicDescription); ++ OSStatus err; ++ listenerSilentFor = 1; ++ AudioObjectPropertyAddress theAddress = { kAudioDevicePropertyStreamFormat, ++ mForInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, ++ kAudioObjectPropertyElementMaster ++ }; ++ err = AudioObjectSetPropertyData(mID, &theAddress, 0, NULL, size, desc); ++ if (err == noErr) { ++ currentNominalSR = desc->mSampleRate; ++ } ++ return err; ++} ++ ++// AudioDeviceGetPropertyInfo() is deprecated, so we wrap AudioObjectGetPropertyDataSize(). ++OSStatus AudioDevice::GetPropertyDataSize(AudioObjectPropertySelector property, UInt32 *size, AudioObjectPropertyAddress *propertyAddress) ++{ ++ AudioObjectPropertyAddress l_propertyAddress; ++ if (!propertyAddress) { ++ propertyAddress = &l_propertyAddress; ++ } ++ propertyAddress->mSelector = property; ++ propertyAddress->mScope = (mForInput) ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput; ++ propertyAddress->mElement = kAudioObjectPropertyElementMaster; ++ ++ return AudioObjectGetPropertyDataSize(mID, propertyAddress, 0, NULL, size); ++} ++ ++int AudioDevice::CountChannels() ++{ ++ OSStatus err; ++ UInt32 propSize; ++ AudioObjectPropertyAddress theAddress; ++ int result = 0; ++ ++ err = GetPropertyDataSize(kAudioDevicePropertyStreamConfiguration, &propSize, &theAddress); ++ if (err) { ++ return 0; ++ } ++ ++ AudioBufferList *buflist = (AudioBufferList *)malloc(propSize); ++ err = AudioObjectGetPropertyData(mID, &theAddress, 0, NULL, &propSize, buflist); ++ if (!err) { ++ for (UInt32 i = 0; i < buflist->mNumberBuffers; ++i) { ++ result += buflist->mBuffers[i].mNumberChannels; ++ } ++ } ++ free(buflist); ++ return result; ++} ++ ++char *AudioDevice::GetName(char *buf, UInt32 maxlen) ++{ ++ if (!buf) { ++ buf = mDevName; ++ maxlen = sizeof(mDevName) / sizeof(char); ++ if (*buf) { ++ return buf; ++ } ++ } ++ AudioObjectPropertyAddress theAddress = { kAudioDevicePropertyDeviceName, ++ mForInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput, ++ kAudioObjectPropertyElementMaster ++ }; // channel ++ ++ verify_noerr(AudioObjectGetPropertyData(mID, &theAddress, 0, NULL, &maxlen, buf)); ++ ++ return buf; ++} ++ ++AudioDevice *AudioDevice::GetDefaultDevice(Boolean forInput, OSStatus &err, AudioDevice *dev) ++{ ++ UInt32 propsize; ++ AudioDeviceID defaultDeviceID; ++ ++ propsize = sizeof(AudioDeviceID); ++ AudioObjectPropertyAddress theAddress = { ++ forInput ? kAudioHardwarePropertyDefaultInputDevice : kAudioHardwarePropertyDefaultOutputDevice, ++ kAudioObjectPropertyScopeGlobal, ++ kAudioObjectPropertyElementMaster ++ }; ++ err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &theAddress, 0, NULL, &propsize, &defaultDeviceID); ++ if (err == noErr) { ++ if (dev) { ++ if (dev->ID() != defaultDeviceID) { ++ delete dev; ++ } else { ++ goto bail; ++ } ++ } ++ dev = new AudioDevice(defaultDeviceID, forInput); ++ } ++bail: ++ ; ++ return dev; ++} ++ ++AudioDevice *AudioDevice::GetDevice(AudioDeviceID devId, Boolean forInput, AudioDevice *dev) ++{ ++ if (dev) { ++ if (dev->ID() != devId) { ++ delete dev; ++ } else { ++ goto bail; ++ } ++ } ++ dev = new AudioDevice(devId, forInput); ++bail: ++ ; ++ return dev; ++} ++ ++// kate: indent-mode cstyle; indent-width 4; replace-tabs on; +diff --git src/modules/PortAudio/3rdparty/CoreAudio/AudioDeviceList.cpp src/modules/PortAudio/3rdparty/CoreAudio/AudioDeviceList.cpp +new file mode 100644 +index 00000000..ba3e00f1 +--- /dev/null ++++ src/modules/PortAudio/3rdparty/CoreAudio/AudioDeviceList.cpp +@@ -0,0 +1,98 @@ ++/* ++ File: AudioDeviceList.cpp ++ Adapted from the CAPlayThough example ++ Version: 1.2.2 ++ ++ Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple ++ Inc. ("Apple") in consideration of your agreement to the following ++ terms, and your use, installation, modification or redistribution of ++ this Apple software constitutes acceptance of these terms. If you do ++ not agree with these terms, please do not use, install, modify or ++ redistribute this Apple software. ++ ++ In consideration of your agreement to abide by the following terms, and ++ subject to these terms, Apple grants you a personal, non-exclusive ++ license, under Apple's copyrights in this original Apple software (the ++ "Apple Software"), to use, reproduce, modify and redistribute the Apple ++ Software, with or without modifications, in source and/or binary forms; ++ provided that if you redistribute the Apple Software in its entirety and ++ without modifications, you must retain this notice and the following ++ text and disclaimers in all such redistributions of the Apple Software. ++ Neither the name, trademarks, service marks or logos of Apple Inc. may ++ be used to endorse or promote products derived from the Apple Software ++ without specific prior written permission from Apple. Except as ++ expressly stated in this notice, no other rights or licenses, express or ++ implied, are granted by Apple herein, including but not limited to any ++ patent rights that may be infringed by your derivative works or by other ++ works in which the Apple Software may be incorporated. ++ ++ The Apple Software is provided by Apple on an "AS IS" basis. APPLE ++ MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ++ THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS ++ FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND ++ OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. ++ ++ IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL ++ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, ++ MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED ++ AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), ++ STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE ++ POSSIBILITY OF SUCH DAMAGE. ++ ++ Copyright (C) 2013 Apple Inc. All Rights Reserved. ++ Copyright (C) 2017,18 René J.V. Bertin All Rights Reserved. ++ ++*/ ++ ++#include "AudioDeviceList.h" ++#include ++ ++#if MAC_OS_X_VERSION_MIN_REQUIRED > 1080 ++ #define verify_noerr __Verify_noErr ++#endif ++ ++AudioDeviceList::AudioDeviceList(bool forInput) ++ : mForInput(forInput) ++{ ++ BuildList(); ++} ++ ++AudioDeviceList::~AudioDeviceList() ++{ ++} ++ ++void AudioDeviceList::BuildList() ++{ ++ mDeviceList.clear(); ++ mDeviceDict.clear(); ++ ++ UInt32 propsize; ++ ++ AudioObjectPropertyAddress theAddress = { kAudioHardwarePropertyDevices, ++ kAudioObjectPropertyScopeGlobal, ++ kAudioObjectPropertyElementMaster ++ }; ++ ++ verify_noerr(AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &theAddress, 0, NULL, &propsize)); ++ int nDevices = propsize / sizeof(AudioDeviceID); ++ AudioDeviceID *devids = new AudioDeviceID[nDevices]; ++ verify_noerr(AudioObjectGetPropertyData(kAudioObjectSystemObject, &theAddress, 0, NULL, &propsize, devids)); ++ ++ for (int i = 0; i < nDevices; ++i) { ++ AudioDevice dev(devids[i], true, mForInput); ++ if (dev.CountChannels() > 0) { ++ Device d; ++ ++ d.mID = devids[i]; ++ dev.GetName(d.mName, sizeof(d.mName)); ++ mDeviceList.push_back(d); ++ QString name = QString::fromUtf8(d.mName); ++ if (!mDeviceDict.contains(name)) { ++ mDeviceDict[name] = d.mID; ++ } ++ } ++ } ++ delete[] devids; ++} +diff --git src/modules/PortAudio/3rdparty/CoreAudio/AudioDeviceList.h src/modules/PortAudio/3rdparty/CoreAudio/AudioDeviceList.h +new file mode 100644 +index 00000000..89ff3a87 +--- /dev/null ++++ src/modules/PortAudio/3rdparty/CoreAudio/AudioDeviceList.h +@@ -0,0 +1,82 @@ ++/* ++ File: AudioDeviceList.h ++ Adapted from the CAPlayThough example ++ Version: 1.2.2 ++ ++ Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple ++ Inc. ("Apple") in consideration of your agreement to the following ++ terms, and your use, installation, modification or redistribution of ++ this Apple software constitutes acceptance of these terms. If you do ++ not agree with these terms, please do not use, install, modify or ++ redistribute this Apple software. ++ ++ In consideration of your agreement to abide by the following terms, and ++ subject to these terms, Apple grants you a personal, non-exclusive ++ license, under Apple's copyrights in this original Apple software (the ++ "Apple Software"), to use, reproduce, modify and redistribute the Apple ++ Software, with or without modifications, in source and/or binary forms; ++ provided that if you redistribute the Apple Software in its entirety and ++ without modifications, you must retain this notice and the following ++ text and disclaimers in all such redistributions of the Apple Software. ++ Neither the name, trademarks, service marks or logos of Apple Inc. may ++ be used to endorse or promote products derived from the Apple Software ++ without specific prior written permission from Apple. Except as ++ expressly stated in this notice, no other rights or licenses, express or ++ implied, are granted by Apple herein, including but not limited to any ++ patent rights that may be infringed by your derivative works or by other ++ works in which the Apple Software may be incorporated. ++ ++ The Apple Software is provided by Apple on an "AS IS" basis. APPLE ++ MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ++ THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS ++ FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND ++ OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. ++ ++ IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL ++ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, ++ MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED ++ AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), ++ STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE ++ POSSIBILITY OF SUCH DAMAGE. ++ ++ Copyright (C) 2013 Apple Inc. All Rights Reserved. ++ Copyright (C) 2017,18 René J.V. Bertin All Rights Reserved. ++ ++*/ ++ ++#ifndef __AudioDeviceList_h__ ++#define __AudioDeviceList_h__ ++ ++#include ++#include ++#include "AudioDevice.h" ++ ++#include ++#include ++ ++class AudioDeviceList { ++public: ++ struct Device { ++ char mName[256]; ++ AudioDeviceID mID; ++ }; ++ typedef QList DeviceList; ++ typedef QHash DeviceDict; ++ ++ AudioDeviceList(bool forInput=false); ++ ~AudioDeviceList(); ++ ++ DeviceList &GetList() { return mDeviceList; } ++ DeviceDict &GetDict() { return mDeviceDict; } ++ ++protected: ++ void BuildList(); ++ ++ bool mForInput; ++ DeviceList mDeviceList; ++ DeviceDict mDeviceDict; ++}; ++ ++#endif // __AudioDeviceList_h__ +diff --git src/modules/PortAudio/CMakeLists.txt src/modules/PortAudio/CMakeLists.txt +index b0310d5c..add7286f 100644 +--- src/modules/PortAudio/CMakeLists.txt ++++ src/modules/PortAudio/CMakeLists.txt +@@ -27,6 +27,13 @@ set(PortAudio_SRC + PortAudioWriter.cpp + ) + ++if(APPLE) ++ list(APPEND PortAudio_SRC ++ 3rdparty/CoreAudio/AudioDevice.mm ++ 3rdparty/CoreAudio/AudioDeviceList.cpp ++ ) ++endif() ++ + set(PortAudio_RESOURCES + icon.qrc + ) +@@ -69,6 +76,9 @@ if(WIN32 AND CUSTOM_PORTAUDIO_LIBRARIES) + target_link_libraries(${PROJECT_NAME} ${CUSTOM_PORTAUDIO_LIBRARIES}) + else() + target_link_libraries(${PROJECT_NAME} ${LIBPORTAUDIO_LIBRARIES}) ++ if(APPLE) ++ target_link_libraries(${PROJECT_NAME} "-framework CoreAudio -framework Foundation") ++ endif() + endif() + + install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION ${MODULES_INSTALL_PATH}) +diff --git src/modules/PortAudio/PortAudio.cpp src/modules/PortAudio/PortAudio.cpp +index a130fe25..5ee2845d 100644 +--- src/modules/PortAudio/PortAudio.cpp ++++ src/modules/PortAudio/PortAudio.cpp +@@ -88,10 +88,19 @@ ModuleSettingsWidget::ModuleSettingsWidget(Module &module) : + int idx = devicesB->findText(sets().getString("OutputDevice")); + devicesB->setCurrentIndex(idx < 0 ? 0 : idx); + ++#ifdef Q_OS_MAC ++ bitPerfect = new QCheckBox(tr("Bit-perfect audio")); ++ bitPerfect->setChecked(sets().getBool("BitPerfect")); ++ bitPerfect->setToolTip(tr("This sets the selected output device to the sample rate of the content being played")); ++#endif ++ + QFormLayout *layout = new QFormLayout(this); + layout->addRow(enabledB); + layout->addRow(tr("Playback device") + ": ", devicesB); + layout->addRow(tr("Delay") + ": ", delayB); ++#ifdef Q_OS_MAC ++ layout->addRow(bitPerfect); ++#endif + } + + void ModuleSettingsWidget::saveSettings() +@@ -99,4 +108,7 @@ void ModuleSettingsWidget::saveSettings() + sets().set("WriterEnabled", enabledB->isChecked()); + sets().set("OutputDevice", devicesB->currentIndex() == 0 ? QString() : devicesB->currentText()); + sets().set("Delay", delayB->value()); ++#ifdef Q_OS_MAC ++ sets().set("BitPerfect", bitPerfect->isChecked()); ++#endif + } +diff --git src/modules/PortAudio/PortAudio.hpp src/modules/PortAudio/PortAudio.hpp +index 5d3d57d7..98a72471 100644 +--- src/modules/PortAudio/PortAudio.hpp ++++ src/modules/PortAudio/PortAudio.hpp +@@ -53,4 +53,7 @@ private: + QCheckBox *enabledB; + QComboBox *devicesB; + QDoubleSpinBox *delayB; ++#ifdef Q_OS_MAC ++ QCheckBox *bitPerfect; ++#endif + }; +diff --git src/modules/PortAudio/PortAudioWriter.cpp src/modules/PortAudio/PortAudioWriter.cpp +index 38dc27d9..a4a6619b 100644 +--- src/modules/PortAudio/PortAudioWriter.cpp ++++ src/modules/PortAudio/PortAudioWriter.cpp +@@ -24,11 +24,17 @@ + #endif + + #ifdef Q_OS_MAC ++ #include "3rdparty/CoreAudio/AudioDeviceList.h" ++ #include "3rdparty/CoreAudio/AudioDevice.h" + #define DEFAULT_HIGH_AUDIO_DELAY 0.2 + #else + #define DEFAULT_HIGH_AUDIO_DELAY 0.1 + #endif + ++#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) ++ #define QStringLiteral QString::fromUtf8 ++#endif ++ + PortAudioWriter::PortAudioWriter(Module &module) : + stream(NULL), + sample_rate(0), +@@ -47,6 +53,13 @@ PortAudioWriter::PortAudioWriter(Module &module) : + } + PortAudioWriter::~PortAudioWriter() + { ++#ifdef Q_OS_MAC ++ if (coreAudioDevice) ++ { ++ coreAudioDevice->ResetNominalSampleRate(); ++ delete coreAudioDevice; ++ } ++#endif + close(); + } + +@@ -191,7 +204,30 @@ void PortAudioWriter::pause() + + QString PortAudioWriter::name() const + { +- return PortAudioWriterName; ++ QString name = PortAudioWriterName; ++ if (stream) ++ { ++ if (const PaDeviceInfo *dInfo = Pa_GetDeviceInfo(outputParameters.device)) ++ { ++ name += QStringLiteral(" (%1").arg(dInfo->name); ++ if (const PaHostApiInfo *hInfo = Pa_GetHostApiInfo(dInfo->hostApi)) ++ { ++ name += QStringLiteral("; %1").arg(hInfo->name); ++ } ++ name += ")"; ++ } ++#ifdef Q_OS_MAC ++ if (const PaStreamInfo *strInfo = Pa_GetStreamInfo(stream)) ++ { ++ name += QStringLiteral(", %1Hz").arg(strInfo->sampleRate); ++ } ++ if (coreAudioDevice) ++ { ++ name += QStringLiteral(" -> %1Hz").arg(coreAudioDevice->CurrentNominalSampleRate()); ++ } ++#endif ++ } ++ return name; + } + + bool PortAudioWriter::open() +@@ -209,6 +245,21 @@ bool PortAudioWriter::openStream() + stream = newStream; + outputLatency = Pa_GetStreamInfo(stream)->outputLatency; + modParam("delay", outputLatency); ++#ifdef Q_OS_MAC ++ if (sets().getBool("BitPerfect")) ++ { ++ const QString devName(Pa_GetDeviceInfo(outputParameters.device)->name); ++ const AudioDeviceList::DeviceDict devDict = AudioDeviceList().GetDict(); ++ if (devDict.contains(devName)) ++ { ++ coreAudioDevice = AudioDevice::GetDevice(devDict[devName], false, coreAudioDevice); ++ if (coreAudioDevice) ++ { ++ coreAudioDevice->SetNominalSampleRate(sample_rate); ++ } ++ } ++ } ++#endif + return true; + } + return false; +diff --git src/modules/PortAudio/PortAudioWriter.hpp src/modules/PortAudio/PortAudioWriter.hpp +index 9d4f617f..009dc013 100644 +--- src/modules/PortAudio/PortAudioWriter.hpp ++++ src/modules/PortAudio/PortAudioWriter.hpp +@@ -25,6 +25,10 @@ + + #include + ++#ifdef Q_OS_MAC ++class AudioDevice; ++#endif ++ + class PortAudioWriter : public Writer + { + Q_DECLARE_TR_FUNCTIONS(PortAudioWriter) +@@ -66,6 +70,9 @@ private: + double outputLatency; + bool err, fullBufferReached; + int underflows; ++#ifdef Q_OS_MAC ++ AudioDevice *coreAudioDevice = nullptr; ++#endif + }; + + #define PortAudioWriterName "PortAudio" diff --git a/multimedia/QMPlay2/files/0010-Do-not-force-fetching-yt-dlp-it-does-not-work-use-Ma.patch b/multimedia/QMPlay2/files/0010-Do-not-force-fetching-yt-dlp-it-does-not-work-use-Ma.patch new file mode 100644 index 0000000000000..2bcb4340a819a --- /dev/null +++ b/multimedia/QMPlay2/files/0010-Do-not-force-fetching-yt-dlp-it-does-not-work-use-Ma.patch @@ -0,0 +1,61 @@ +From 2a99e595ce53c0ef1ecef0fad67319dff0001fec Mon Sep 17 00:00:00 2001 +From: Sergey Fedorov +Date: Sat, 30 Nov 2024 03:04:33 +0800 +Subject: [PATCH] =?UTF-8?q?Do=20not=20force=20fetching=20yt-dlp,=20i?= + =?UTF-8?q?t=20does=20not=20work;=20use=20MacPorts=E2=80=99=20one?= +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +--- + src/qmplay2/YouTubeDL.cpp | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git src/qmplay2/YouTubeDL.cpp src/qmplay2/YouTubeDL.cpp +index 98071fea..b87f64af 100644 +--- src/qmplay2/YouTubeDL.cpp ++++ src/qmplay2/YouTubeDL.cpp +@@ -28,6 +28,11 @@ + #include + #include + ++/* Avoid downloading yt-dlp, it fails to work correctly. */ ++#ifndef BUNDLED_YTDLP ++#define BUNDLED_YTDLP 0 ++#endif ++ + constexpr char g_name[] = "YouTubeDL"; + + static QReadWriteLock g_lock; +@@ -263,6 +268,7 @@ QStringList YouTubeDL::exec(const QString &url, const QStringList &args, QString + if (error.indexOf("ERROR: ") == 0) + error.remove(0, 7); + } ++ #if BUNDLED_YTDLP + if (canUpdate && !error.contains("said:")) // Probably update can fix the error, so do it! + { + if (!doLock(Lock::Write, true)) // Unlock for read and lock for write +@@ -300,6 +306,7 @@ QStringList YouTubeDL::exec(const QString &url, const QStringList &args, QString + if (!doLock(Lock::Read, true)) // Unlock for write and lock for read + return {}; + } ++ #endif + finishWithError(error); + return {}; + } +@@ -317,6 +324,7 @@ QStringList YouTubeDL::exec(const QString &url, const QStringList &args, QString + g_lock.unlock(); // Unlock for read + return result; + } ++#if BUNDLED_YTDLP + else if (canUpdate && !m_aborted && m_process.error() == QProcess::FailedToStart) + { + const QString downloadUrl = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp" +@@ -361,6 +369,7 @@ QStringList YouTubeDL::exec(const QString &url, const QStringList &args, QString + QMPlay2Core.setWorking(false); + } + } ++#endif + + g_lock.unlock(); // Unlock for read or for write (if download has failed) + #else diff --git a/multimedia/QMPlay2/files/0011-YouTubeDL-move-to-QJson.patch b/multimedia/QMPlay2/files/0011-YouTubeDL-move-to-QJson.patch new file mode 100644 index 0000000000000..a0fe30dead1fb --- /dev/null +++ b/multimedia/QMPlay2/files/0011-YouTubeDL-move-to-QJson.patch @@ -0,0 +1,88 @@ +From a22054bb93586bdaeb1e29f90ef2367d90fe2d34 Mon Sep 17 00:00:00 2001 +From: Sergey Fedorov +Date: Fri, 29 Nov 2024 10:41:22 +0800 +Subject: [PATCH] YouTubeDL: move to QJson + +--- + src/modules/Extensions/MediaBrowser/ProstoPleer.cpp | 7 ++++--- + src/qmplay2/CMakeLists.txt | 3 ++- + src/qmplay2/YouTubeDL.cpp | 13 +++++++------ + 3 files changed, 13 insertions(+), 10 deletions(-) + +diff --git src/modules/Extensions/MediaBrowser/ProstoPleer.cpp src/modules/Extensions/MediaBrowser/ProstoPleer.cpp +index cb3c287d..c35bd5d2 100644 +--- src/modules/Extensions/MediaBrowser/ProstoPleer.cpp ++++ src/modules/Extensions/MediaBrowser/ProstoPleer.cpp +@@ -21,9 +21,10 @@ + #include + #include + #include +-#include + ++#include + #include ++#include + #include + #include + #include +@@ -187,8 +188,8 @@ bool ProstoPleer::convertAddress(const QString &prefix, const QString &url, cons + IOController &netReply = ioCtrl->toRef(); + if (net.startAndWait(netReply, QString("%1/site_api/files/get_url?id=%2").arg(g_url, fileId.mid(idx + 1)))) + { +- const Json json = Json::parse(netReply->readAll()); +- const QString tmpStreamUrl = json["track_link"].string_value(); ++ const QJsonDocument json = QJsonDocument::fromJson(netReply->readAll()); ++ const QString tmpStreamUrl = json.object()["track_link"].toString(); + if (!tmpStreamUrl.isEmpty()) + *streamUrl = tmpStreamUrl; + netReply.clear(); +diff --git src/qmplay2/CMakeLists.txt src/qmplay2/CMakeLists.txt +index 01932ce6..270ad9cf 100644 +--- src/qmplay2/CMakeLists.txt ++++ src/qmplay2/CMakeLists.txt +@@ -212,7 +212,8 @@ if(USE_QT5) + target_link_libraries(${PROJECT_NAME} ${APPKIT_LIBRARY}) + endif() + else() +- target_link_libraries(${PROJECT_NAME} Qt4::QtCore Qt4::QtGui ${DBUS}) ++ add_definitions(-I/opt/local/include/QJson4) ++ target_link_libraries(${PROJECT_NAME} Qt4::QtCore Qt4::QtGui QJson4 ${DBUS}) + endif() + + if(WIN32) +diff --git src/qmplay2/YouTubeDL.cpp src/qmplay2/YouTubeDL.cpp +index b87f64af..fb7e72fc 100644 +--- src/qmplay2/YouTubeDL.cpp ++++ src/qmplay2/YouTubeDL.cpp +@@ -21,11 +21,13 @@ + #include + #include + #include +-#include + #include + + #include + #include ++#include ++#include ++#include + #include + + /* Avoid downloading yt-dlp, it fails to work correctly. */ +@@ -41,12 +43,11 @@ static QReadWriteLock g_lock; + + static void exportCookiesFromJSON(const QString &jsonData, const QString &url) + { +- const Json json = Json::parse(jsonData.toUtf8()); +- const QByteArray urlData = url.toUtf8(); +- for (const Json &formats : json["formats"].array_items()) ++ const QJsonDocument json = QJsonDocument::fromJson(jsonData.toUtf8()); ++ for (const QJsonValue &formats : json.object()["formats"].toArray()) + { +- if (urlData == formats["url"].string_value()) +- QMPlay2Core.addCookies(url, formats["http_headers"]["Cookie"].string_value()); ++ if (url == formats.toObject()["url"].toString()) ++ QMPlay2Core.addCookies(url, formats.toObject()["http_headers"].toObject()["Cookie"].toString().toUtf8()); + } + } + diff --git a/multimedia/QMPlay2/files/0007-Fix-Qt-paths.patch b/multimedia/QMPlay2/files/0012-Fix-Qt-paths.patch similarity index 100% rename from multimedia/QMPlay2/files/0007-Fix-Qt-paths.patch rename to multimedia/QMPlay2/files/0012-Fix-Qt-paths.patch