diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 2ce6e1b..e373615 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -17,7 +17,7 @@ jobs: # Create the AppImage AppImage: name: Create the AppImage - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Install the AppImage bundler and Performous deps id: fetch_deps @@ -26,11 +26,11 @@ jobs: chmod +x appimage-builder-x86_64.AppImage sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder sudo apt update - sudo apt-get install -y --no-install-recommends git cmake build-essential gettext help2man libavcodec-dev libavformat-dev libswscale-dev qtbase5-dev qtmultimedia5-dev ca-certificates file + sudo apt-get install -y --no-install-recommends git cmake build-essential gettext help2man libavcodec-dev libavformat-dev libswscale-dev qtbase5-dev qtmultimedia5-dev ca-certificates file libfuse2 - name: Checkout Git id: checkout_git - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build the AppImage id: build_appimage @@ -52,7 +52,7 @@ jobs: # Upload artifacts during pull-requests - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ github.event_name == 'pull_request' }} with: name: ${{ env.ARTIFACT_NAME }} @@ -61,7 +61,7 @@ jobs: # Upload artifacts on master - name: Upload artifact with unified name if: ${{ github.ref == 'refs/heads/master' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.MASTER_ARTIFACT_NAME }} path: ${{ env.MASTER_ARTIFACT_PATH }} diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 6383675..4f803dd 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout id: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -55,18 +55,22 @@ jobs: version: 20.04 - os: ubuntu version: 22.04 + - os: ubuntu + version: 24.04 - os: debian - version: 10 - - os: debian - version: 11 + version: 12 + - os: fedora + version: 36 + - os: fedora + version: 37 - os: fedora - version: 34 + version: 38 - os: fedora - version: 35 - ## FFMPEG5 causes issues - ## See https://github.com/performous/composer/issues/45 - #- os: fedora - # version: 36 + version: 39 + - os: fedora + version: 40 + - os: fedora + version: 41 steps: - name: Container name run: | @@ -75,10 +79,10 @@ jobs: echo "CONTAINER_NAME=${BUILD_CONTAINER}" >> $GITHUB_ENV - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Login to the container registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ env.REPO_NAME }} @@ -129,7 +133,7 @@ jobs: # Upload artifacts during pull-requests - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ github.event_name == 'pull_request' }} with: name: ${{ env.ARTIFACT_NAME }} @@ -138,7 +142,7 @@ jobs: # Upload artifacts on master - name: Upload artifact with unified name if: ${{ github.ref == 'refs/heads/master' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.MASTER_ARTIFACT_NAME }} path: ${{ env.MASTER_ARTIFACT_PATH }} @@ -157,7 +161,7 @@ jobs: asset_content_type: application/octet-stream - name: Push container - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 # Containers can't be pushed during PRs because of the way permissions # are delegated to secrets.GITHUB_TOKEN if: ${{ needs.determine_docker_build.outputs.build_docker_containers == 'true' && github.event_name != 'pull_request' }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 99c71db..6e617f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ +cmake_minimum_required(VERSION 3.10) +cmake_policy(VERSION 3.10) project("Composer" CXX C) -cmake_minimum_required(VERSION 3.0) -cmake_policy(VERSION 3.0) set(PROJECT_VERSION "2.0.1") set(BUILD_SHARED_LIBS OFF) #FIXME: Changes in version number or project name must manually also be put to: diff --git a/README.md b/README.md index 9613cd6..d271061 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,14 @@ Latest builds ========== - [Linux - Ubuntu 20.04](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-ubuntu_20.04.deb.zip) - [Linux - Ubuntu 22.04](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-ubuntu_22.04.deb.zip) -- [Linux - Debian 10](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-debian_10.deb.zip) -- [Linux - Debian 11](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-debian_11.deb.zip) -- [Linux - Fedora 34](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_34.rpm.zip) -- [Linux - Fedora 35](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_35.rpm.zip) +- [Linux - Ubuntu 24.04](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-ubuntu_24.04.deb.zip) +- [Linux - Debian 12](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-debian_12.deb.zip) +- [Linux - Fedora 36](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_36.rpm.zip) +- [Linux - Fedora 37](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_37.rpm.zip) +- [Linux - Fedora 38](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_38.rpm.zip) +- [Linux - Fedora 39](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_39.rpm.zip) +- [Linux - Fedora 40](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_40.rpm.zip) +- [Linux - Fedora 41](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_41.rpm.zip) - [Linux - AppImage](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest.AppImage.zip) Build & Install diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e32bc39..3e93dec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,5 @@ -cmake_minimum_required(VERSION 3.0) -cmake_policy(VERSION 3.0) +cmake_minimum_required(VERSION 3.10) +cmake_policy(VERSION 3.10) set(EXENAME ${CMAKE_PROJECT_NAME}) set(CMAKE_AUTOMOC FALSE) @@ -40,8 +40,8 @@ target_sources(${EXENAME} PRIVATE ${HEADER_FILES} ${SOURCE_FILES} ${MOC_SOURCES} # Generate config.hh configure_file(config.cmake.hh "${CMAKE_BINARY_DIR}/src/config.hh" @ONLY) -include_directories(${CMAKE_BINARY_DIR}/src) -include_directories(${CMAKE_SOURCE_DIR}/src) +target_include_directories(${EXENAME} PRIVATE ${CMAKE_BINARY_DIR}/src) +target_include_directories(${EXENAME} PRIVATE ${CMAKE_SOURCE_DIR}/src) # We don't currently have any assets, so on Windows, we just install to the root installation folder if(UNIX) diff --git a/src/config.cmake.hh b/src/config.cmake.hh index f470945..a0c47e9 100644 --- a/src/config.cmake.hh +++ b/src/config.cmake.hh @@ -13,11 +13,13 @@ // FFMPEG libraries use changing include file names... Get them from CMake. #define AVCODEC_INCLUDE <@AVCodec_INCLUDE@> +#define AVCODEC_CODEC_PAR_INCLUDE #define AVFORMAT_INCLUDE <@AVFormat_INCLUDE@> #define SWRESAMPLE_INCLUDE <@SWResample_INCLUDE@> #define SWSCALE_INCLUDE <@SWScale_INCLUDE@> #define AVUTIL_INCLUDE <@AVUtil_INCLUDE@> #define AVUTIL_OPT_INCLUDE //HACK to get AVOption class! #define AVUTIL_MATH_INCLUDE +#define AVUTIL_ERROR_INCLUDE #endif diff --git a/src/ffmpeg.cc b/src/ffmpeg.cc index 32fc6b2..c045fea 100644 --- a/src/ffmpeg.cc +++ b/src/ffmpeg.cc @@ -8,36 +8,43 @@ // Somehow ffmpeg headers give errors that these are not defined... #define AVCODEC_MAX_AUDIO_FRAME_SIZE 192000 extern "C" { -#include AVCODEC_INCLUDE -#include AVFORMAT_INCLUDE -#include SWSCALE_INCLUDE -#include SWRESAMPLE_INCLUDE -#include AVUTIL_INCLUDE -#include AVUTIL_OPT_INCLUDE -#include AVUTIL_MATH_INCLUDE + #include AVCODEC_INCLUDE +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(58,87,100) // FFmpeg 4.3 + #include AVCODEC_CODEC_PAR_INCLUDE +#endif + #include AVFORMAT_INCLUDE + #include SWSCALE_INCLUDE + #include SWRESAMPLE_INCLUDE + #include AVUTIL_INCLUDE + #include AVUTIL_OPT_INCLUDE + #include AVUTIL_MATH_INCLUDE + #include AVUTIL_ERROR_INCLUDE } -/// A custom allocator that uses av_malloc for aligned buffers -template class AvMalloc: public std::allocator { -public: -/* pointer allocate(size_type count, allocator::const_pointer* = 0) { - pointer ptr = static_cast(m(count * sizeof(T))); - if (!ptr) throw std::bad_alloc(); - return ptr; - } - void deallocate(pointer ptr, size_type) { f(ptr); }*/ -private: - void* m(size_t n) { return av_malloc(n); } - void f(void* ptr) { av_free(ptr); } -}; +QMutex FFmpeg::s_avcodec_mutex; +void AVFormatContextDeleter::operator ()(AVFormatContext* context) { + avformat_close_input(&context); +} -/*static*/ QMutex FFmpeg::s_avcodec_mutex; +void AVCodecContextDeleter::operator ()(AVCodecContext* context) { + avcodec_free_context(&context); +} + +void SwrContextDeleter::operator ()(SwrContext* context) { + swr_free(&context); +} + +static std::string stringFromErrorCode(int code) { + char buffer[AV_ERROR_MAX_STRING_SIZE]; + av_make_error_string(buffer, sizeof(buffer), code); + return buffer; +} FFmpeg::FFmpeg(std::string const& _filename): m_filename(_filename), m_quit(), m_running(), m_eof(), - pFormatCtx(), pAudioCodecCtx(), pAudioCodec(), m_rate(48000), - audioStream(-1), m_position() + pFormatCtx(), pAudioCodecCtx(), m_rate(48000), + audioStream(-1) { open(); // Throws on error m_running = true; @@ -51,8 +58,6 @@ FFmpeg::~FFmpeg() { wait(); // TODO: use RAII for freeing resources (to prevent memory leaks) QMutexLocker l(&s_avcodec_mutex); // avcodec_close is not thread-safe - if (pAudioCodecCtx) avcodec_close(pAudioCodecCtx); - if (pFormatCtx) avformat_close_input(&pFormatCtx); } double FFmpeg::duration() const { @@ -60,43 +65,73 @@ double FFmpeg::duration() const { return d >= 0.0 ? d : getInf(); } -// FFMPEG has fluctuating API -#if LIBAVCODEC_VERSION_INT < ((52<<16)+(64<<8)+0) -#define AVMEDIA_TYPE_VIDEO CODEC_TYPE_VIDEO -#define AVMEDIA_TYPE_AUDIO CODEC_TYPE_AUDIO -#endif - void FFmpeg::open() { QMutexLocker l(&s_avcodec_mutex); +#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58,9,100) // Got deprecated in 4.0 but needed for earlier versions av_register_all(); +#endif av_log_set_level(AV_LOG_ERROR); - if (avformat_open_input(&pFormatCtx, m_filename.c_str(), NULL, NULL)) throw std::runtime_error("Cannot open input file"); - if (avformat_find_stream_info(pFormatCtx, NULL) < 0) throw std::runtime_error("Cannot find stream information"); + + AVFormatContext *ps{}; + + auto err = avformat_open_input(&ps, m_filename.c_str(), NULL, NULL); + if (err != 0) + throw std::runtime_error(std::string("Cannot open input file. Error: ") + std::to_string(err) + " " + stringFromErrorCode(err)); + + pFormatCtx = {ps, {}}; + + if (avformat_find_stream_info(pFormatCtx.get(), NULL) < 0) throw std::runtime_error("Cannot find stream information"); pFormatCtx->flags |= AVFMT_FLAG_GENPTS; audioStream = -1; + // Take the first video/audio streams for (unsigned int i=0; inb_streams; i++) { - AVCodecContext* cc = pFormatCtx->streams[i]->codec; - cc->workaround_bugs = FF_BUG_AUTODETECT; - if (audioStream == -1 && cc->codec_type==AVMEDIA_TYPE_AUDIO) audioStream = i; + auto * codecpar = pFormatCtx->streams[i]->codecpar; + + if (codecpar->codec_type==AVMEDIA_TYPE_AUDIO) + { + audioStream = i; + break; + } } if (audioStream == -1) throw std::runtime_error("No audio stream found"); - AVCodecContext* cc = pFormatCtx->streams[audioStream]->codec; - pAudioCodec = avcodec_find_decoder(cc->codec_id); + + auto* codecpar = pFormatCtx->streams[audioStream]->codecpar; + const auto* pAudioCodec = avcodec_find_decoder(codecpar->codec_id); audioQueue.setRateChannels(m_rate, 2); if (!pAudioCodec) throw std::runtime_error("Cannot find audio codec"); - if (avcodec_open2(cc, pAudioCodec, NULL) < 0) throw std::runtime_error("Cannot open audio codec"); - pAudioCodecCtx = cc; - m_resampleContext = swr_alloc(); - av_opt_set_int(m_resampleContext, "in_channel_layout", pAudioCodecCtx->channel_layout ? pAudioCodecCtx->channel_layout : av_get_default_channel_layout(pAudioCodecCtx->channels), 0); - av_opt_set_int(m_resampleContext, "out_channel_layout", av_get_default_channel_layout(2), 0); - av_opt_set_int(m_resampleContext, "in_sample_rate", pAudioCodecCtx->sample_rate, 0); - av_opt_set_int(m_resampleContext, "out_sample_rate", m_rate, 0); - av_opt_set_int(m_resampleContext, "in_sample_fmt", pAudioCodecCtx->sample_fmt, 0); - av_opt_set_int(m_resampleContext, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0); - swr_init(m_resampleContext); - if (!m_resampleContext) throw std::runtime_error("Cannot create resampling context"); + pAudioCodecCtx = {avcodec_alloc_context3(pAudioCodec), {}}; + avcodec_parameters_to_context(pAudioCodecCtx.get(), codecpar); + pAudioCodecCtx->workaround_bugs = 1; + + if (avcodec_open2(pAudioCodecCtx.get(), pAudioCodec, NULL) < 0) throw std::runtime_error("Cannot open audio codec"); + m_resampleContext = {swr_alloc(), {}}; + +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 24, 100) // FFmpeg 5.1 and up + AVChannelLayout in_chlayout{}; + if (av_channel_layout_copy(&in_chlayout, &pAudioCodecCtx->ch_layout) < 0) + { + av_channel_layout_default(&in_chlayout, pAudioCodecCtx->ch_layout.nb_channels); + }; + AVChannelLayout out_chlayout{}; + av_channel_layout_default(&out_chlayout, 2); + // av_get_default_channel_layout(2) +#endif + +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 24, 100) // FFmpeg 5.1 and up + av_opt_set_chlayout(m_resampleContext.get(), "in_chlayout", &in_chlayout, 0); + av_opt_set_chlayout(m_resampleContext.get(), "out_chlayout", &out_chlayout, 0); +#else // FFmpeg 5.0 and earlier + av_opt_set_int(m_resampleContext.get(), "in_channel_layout", pAudioCodecCtx->channel_layout ? pAudioCodecCtx->channel_layout : av_get_default_channel_layout(pAudioCodecCtx->channels), 0); + av_opt_set_int(m_resampleContext.get(), "out_channel_layout", av_get_default_channel_layout(2), 0); +#endif + av_opt_set_int(m_resampleContext.get(), "in_sample_rate", pAudioCodecCtx->sample_rate, 0); + av_opt_set_int(m_resampleContext.get(), "out_sample_rate", m_rate, 0); + av_opt_set_int(m_resampleContext.get(), "in_sample_fmt", pAudioCodecCtx->sample_fmt, 0); + av_opt_set_int(m_resampleContext.get(), "out_sample_fmt", AV_SAMPLE_FMT_S16, 0); + swr_init(m_resampleContext.get()); + if (!m_resampleContext) throw std::runtime_error("Cannot create resampling context"); } void FFmpeg::run() { @@ -138,7 +173,7 @@ void FFmpeg::seek_internal() { // Latest Performous code does not have these lines //const AVRational time_base_q = { 1, AV_TIME_BASE }; // AV_TIME_BASE_Q is the same thing with C99 struct literal (not supported by MSVC) //if (stream != -1) target = av_rescale_q(target, time_base_q, pFormatCtx->streams[stream]->time_base); - av_seek_frame(pFormatCtx, stream, target, flags); + av_seek_frame(pFormatCtx.get(), stream, target, flags); m_seekTarget = getNaN(); // Signal that seeking is done } @@ -155,40 +190,55 @@ void FFmpeg::decodeNextFrame() { } }; - int frameFinished=0; + struct ReadFrame + { + AVFrame* m_frame; + + ReadFrame(): m_frame(av_frame_alloc()) { + if (m_frame == nullptr) throw eof_error(); + } + ~ReadFrame() { av_frame_free(&m_frame); } + + operator AVFrame*() {return m_frame;} + AVFrame operator*() {return *m_frame;} + AVFrame* operator->() {return m_frame;} + }; + + bool frameFinished{}; while (!frameFinished) { - ReadFramePacket packet(pFormatCtx); - unsigned packetPos = 0; + ReadFramePacket packet(pFormatCtx.get()); if (packet.stream_index==audioStream) { - while (packetPos < packet.size) { + auto err = avcodec_send_packet(pAudioCodecCtx.get(), &packet); + if (err == AVERROR_EOF) break; // Nothing we can do + if (err != 0 && err != AVERROR(EAGAIN)) throw std::runtime_error(std::string("Can't send packet. Error: ") + std::to_string(err) + " " + stringFromErrorCode(err)); + + do + { if (m_quit || m_seekTarget == m_seekTarget) return; - int bytesUsed; - #if (LIBAVCODEC_VERSION_INT) > (AV_VERSION_INT(55,0,0)) - AVFrame* m_frame = av_frame_alloc(); - #else - AVFrame* m_frame = avcodec_alloc_frame(); - #endif - int gotFramePointer = 0; - bytesUsed = avcodec_decode_audio4(pAudioCodecCtx,m_frame,&gotFramePointer, &packet); - if (bytesUsed < 0) throw std::runtime_error("cannot decode audio frame"); - packetPos += bytesUsed; - // Update position if timecode is available - if (packet.time() == packet.time()) m_position = packet.time(); + ReadFrame m_frame{}; + + err = avcodec_receive_frame(pAudioCodecCtx.get(), m_frame); + if (err == 0); + else if (err == AVERROR(EAGAIN)) break; + else if (err == AVERROR_EOF) return; // Finished file + else throw std::runtime_error(std::string("cannot decode audio frame. Error: ") + std::to_string(err) + " " + stringFromErrorCode(err)); + //resample here! int16_t * output; int out_linesize; - int out_samples = swr_get_out_samples(m_resampleContext, m_frame->nb_samples); + int out_samples = swr_get_out_samples(m_resampleContext.get(), m_frame->nb_samples); av_samples_alloc((uint8_t**)&output, &out_linesize, 2, out_samples,AV_SAMPLE_FMT_S16, 0); - out_samples = swr_convert(m_resampleContext, (uint8_t**)&output, out_samples, (const uint8_t**)&m_frame->data[0], m_frame->nb_samples); + out_samples = swr_convert(m_resampleContext.get(), (uint8_t**)&output, out_samples, (const uint8_t**)&m_frame->data[0], m_frame->nb_samples); std::vector m_output(output, output+out_samples*2); // Output samples int outsize = m_output.size(); /* Convert bytes into samples */ \ short* samples = reinterpret_cast(&m_output[0]);\ audioQueue.input(samples, samples + outsize, 1.0 / 32767.0);\ - m_position += double(out_samples)/m_rate; // New position in case the next packet doesn't have packet.time() } + while (true); + // Audio frames are always finished - frameFinished = 1; + frameFinished = true; } } } diff --git a/src/ffmpeg.hh b/src/ffmpeg.hh index 58f1e71..c7016ef 100644 --- a/src/ffmpeg.hh +++ b/src/ffmpeg.hh @@ -1,12 +1,12 @@ #pragma once -#include "util.hh" #include "libda/sample.hpp" #include #include #include #include #include + #include #include @@ -86,6 +86,21 @@ extern "C" { struct SwsContext; } +struct AVFormatContextDeleter +{ + void operator ()(AVFormatContext*); +}; + +struct AVCodecContextDeleter +{ + void operator ()(AVCodecContext*); +}; + +struct SwrContextDeleter +{ + void operator ()(SwrContext*); +}; + /// ffmpeg class class FFmpeg: public QThread { public: @@ -113,13 +128,11 @@ class FFmpeg: public QThread { volatile bool m_running; volatile bool m_eof; volatile double m_seekTarget; - AVFormatContext* pFormatCtx; - SwrContext* m_resampleContext; - AVCodecContext* pAudioCodecCtx; - AVCodec* pAudioCodec; + std::unique_ptr pFormatCtx; + std::unique_ptr m_resampleContext; + std::unique_ptr pAudioCodecCtx; int audioStream; - double m_position; static QMutex s_avcodec_mutex; // Used for avcodec_open/close (which use some static crap and are thus not thread-safe) };