From 788d8e7a0198475aa427ff34ccb9ace3d6d9f773 Mon Sep 17 00:00:00 2001 From: Flaminator Date: Sun, 6 Apr 2025 23:44:23 +0200 Subject: [PATCH 1/4] Update the cmake version to 3.10 so that one can build this project when using cmake 4.0 which deprecated versions below 3.10 --- CMakeLists.txt | 4 ++-- src/CMakeLists.txt | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) 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/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) From b7dc6dad87a9361d99d83da2cbcaca4451fcbd90 Mon Sep 17 00:00:00 2001 From: Flaminator Date: Sun, 6 Apr 2025 23:56:09 +0200 Subject: [PATCH 2/4] Update the ffmpeg code being used to add support for newer versions of FFmpeg This code builds with FFmpeg versions 3.1..3.4, 4.0..4.4, 5.0, 5.1, 6.0, 6.1, 7.0 and 7.1. Because FFmpeg changed a lot support for 3.0 and earlier has been dropped, 3.0 being released on 2016-02-14. --- src/config.cmake.hh | 2 + src/ffmpeg.cc | 189 ++++++++++++++++++++++++++++---------------- src/ffmpeg.hh | 25 ++++-- 3 files changed, 140 insertions(+), 76 deletions(-) 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..d15cb16 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); } -}; +/*static*/ 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,72 @@ 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{}; + + if (auto err = avformat_open_input(&ps, m_filename.c_str(), NULL, NULL); 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 +172,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 +189,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) }; From 724a81a3e30d431b2abc34f3684e67ba5f26e6d3 Mon Sep 17 00:00:00 2001 From: Flaminator Date: Mon, 7 Apr 2025 02:20:29 +0200 Subject: [PATCH 3/4] Change the init-statement in the if statement to have it outside of the if statement Seems the msvc build being used to compile it for Windows doesn't support the c++17 feature --- src/ffmpeg.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ffmpeg.cc b/src/ffmpeg.cc index d15cb16..c045fea 100644 --- a/src/ffmpeg.cc +++ b/src/ffmpeg.cc @@ -21,7 +21,7 @@ extern "C" { #include AVUTIL_ERROR_INCLUDE } -/*static*/ QMutex FFmpeg::s_avcodec_mutex; +QMutex FFmpeg::s_avcodec_mutex; void AVFormatContextDeleter::operator ()(AVFormatContext* context) { avformat_close_input(&context); @@ -74,7 +74,8 @@ void FFmpeg::open() { AVFormatContext *ps{}; - if (auto err = avformat_open_input(&ps, m_filename.c_str(), NULL, NULL); err != 0) + 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, {}}; From 29c16e48b3d04c29084151f1630b264de111199d Mon Sep 17 00:00:00 2001 From: Arjan Speiard | Asgard Sings! Date: Wed, 9 Apr 2025 12:48:58 +0200 Subject: [PATCH 4/4] Updated GithubActions to reflect deprecated and removed actions. --- .github/workflows/appimage.yml | 10 +++++----- .github/workflows/linux.yml | 34 +++++++++++++++++++--------------- README.md | 12 ++++++++---- 3 files changed, 32 insertions(+), 24 deletions(-) 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/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