From 738eb8ed2865f1b24c79b2c89f947c713cab6ecd Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 12 Mar 2026 22:34:24 +0100 Subject: [PATCH 01/40] CI: add linux appimage action --- .github/build.yml | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/build.yml diff --git a/.github/build.yml b/.github/build.yml new file mode 100644 index 0000000..756d188 --- /dev/null +++ b/.github/build.yml @@ -0,0 +1,42 @@ +name: Build eismuliplexer for linux +run-name: Building eismuliplexer for linux +on: [push] + +jobs: + Build: + runs-on: ubuntu-latest + steps: + - name: Install dependancies + run: apt update; apt install -y libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 libqt6multimedia6 wget libglx-dev libgl1-mesa-dev qt6-qpa-plugins + - name: Check out repository code + uses: ischanx/checkout@8c80eac3058d03dc5301629e8f7d59ae255d6cc3 + - name: Checkout libeismultiplexer + run: git clone http://192.168.178.27/git/Eismultiplexer/libeismultiplexer.git + - name: Install Appimagetool + run: | + wget -c https://github.com/$(wget -q https://github.com/probonopd/go-appimage/releases/expanded_assets/continuous -O - | grep "appimagetool-.*-x86_64.AppImage" | head -n 1 | cut -d '"' -f 2) + chmod +x appimagetool-*-x86_64.AppImage + mv appimagetool-*-x86_64.AppImage /usr/bin/appimagetool + - name: Version + id: version + run: | + cd ${{ gitea.workspace }} + git fetch -a; + echo "tag=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT + - name: Build + run: | + mkdir ${{ gitea.workspace }}/build; cd ${{ gitea.workspace }}/build + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=./install/usr -DGIT_TAG=${{ steps.version.outputs.tag }} .. + make -j $(nproc) + make install + - name: Create Appimage + run: | + cd ${{ gitea.workspace }}/build/ + appimagetool --appimage-extract-and-run -s deploy install/usr/share/applications/xyz.uvos.aceradio.desktop + appimagetool --appimage-extract-and-run -s install + - name: Upload Appimage + uses: actions/upload-artifact@v6 + with: + name: aceradio-appimage + path: ${{ gitea.workspace }}/build/*.AppImage + From f8202fc3ab0b06506b1785f59b628d418ce73c33 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 12 Mar 2026 22:36:45 +0100 Subject: [PATCH 02/40] move workflow to correct location --- .github/{ => workflows}/build.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{ => workflows}/build.yml (100%) diff --git a/.github/build.yml b/.github/workflows/build.yml similarity index 100% rename from .github/build.yml rename to .github/workflows/build.yml From 67a99070b2aaffc47ae79ca92a564cba8c1530ac Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 12 Mar 2026 22:38:35 +0100 Subject: [PATCH 03/40] We are now running on github not gitea --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 756d188..58e13ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,23 +20,23 @@ jobs: - name: Version id: version run: | - cd ${{ gitea.workspace }} + cd ${{ github.workspace }} git fetch -a; echo "tag=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT - name: Build run: | - mkdir ${{ gitea.workspace }}/build; cd ${{ gitea.workspace }}/build + mkdir ${{ github.workspace }}/build; cd ${{ github.workspace }}/build cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=./install/usr -DGIT_TAG=${{ steps.version.outputs.tag }} .. make -j $(nproc) make install - name: Create Appimage run: | - cd ${{ gitea.workspace }}/build/ + cd ${{ github.workspace }}/build/ appimagetool --appimage-extract-and-run -s deploy install/usr/share/applications/xyz.uvos.aceradio.desktop appimagetool --appimage-extract-and-run -s install - name: Upload Appimage uses: actions/upload-artifact@v6 with: name: aceradio-appimage - path: ${{ gitea.workspace }}/build/*.AppImage + path: ${{ github.workspace }}/build/*.AppImage From 1f0a6c271856c0da1e5ddf35e2238aa5443dc05c Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 12 Mar 2026 22:41:13 +0100 Subject: [PATCH 04/40] Github needs sudo for apt --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58e13ea..c9fb341 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Install dependancies - run: apt update; apt install -y libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 libqt6multimedia6 wget libglx-dev libgl1-mesa-dev qt6-qpa-plugins + run: sudo apt update; sudo apt install -y libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 libqt6multimedia6 wget libglx-dev libgl1-mesa-dev qt6-qpa-plugins - name: Check out repository code uses: ischanx/checkout@8c80eac3058d03dc5301629e8f7d59ae255d6cc3 - name: Checkout libeismultiplexer From 2118db5fb0e1c232236049abcf187f961b6475d3 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Thu, 12 Mar 2026 22:34:24 +0100 Subject: [PATCH 05/40] CI: add linux appimage action --- .github/workflows/build.yml | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5ba0ee4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,42 @@ +name: Build eismuliplexer for linux +run-name: Building eismuliplexer for linux +on: [push] + +jobs: + Build: + runs-on: ubuntu-latest + steps: + - name: Install dependancies + run: sudo apt update; sudo apt install -y libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 libqt6multimedia6 qt6-multimedia-dev ffmpeg wget libglx-dev libgl1-mesa-dev qt6-qpa-plugins + - name: Check out repository code + uses: actions/checkout@v6 + - name: Install Appimagetool + run: | + wget -c https://github.com/$(wget -q https://github.com/probonopd/go-appimage/releases/expanded_assets/continuous -O - | grep "appimagetool-.*-x86_64.AppImage" | head -n 1 | cut -d '"' -f 2) + chmod +x appimagetool-*-x86_64.AppImage + sudo mv appimagetool-*-x86_64.AppImage /usr/bin/appimagetool + - name: Version + id: version + run: | + cd ${{ github.workspace }} + git fetch -a; + echo "tag=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT + - name: Build + run: | + mkdir ${{ github.workspace }}/build; cd ${{ github.workspace }}/build + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=./install/usr -DGIT_TAG=${{ steps.version.outputs.tag }} .. + make -j $(nproc) + make install + - name: Create Appimage + run: | + cd ${{ github.workspace }}/build/ + install -d install/usr/lib/x86_64-linux-gnu/qt6/plugins/multimedia + cp -vr /lib/x86_64-linux-gnu/qt6/plugins/multimedia/libffmpegmediaplugin.so install/usr/lib/x86_64-linux-gnu/qt6/plugins/multimedia/ + appimagetool --appimage-extract-and-run -s deploy install/usr/share/applications/xyz.uvos.aceradio.desktop + appimagetool --appimage-extract-and-run -s install + - name: Upload Appimage + uses: actions/upload-artifact@v6 + with: + name: aceradio-appimage + path: ${{ github.workspace }}/build/*.AppImage + From 06d15ecfefcaa022c33d32248416a1ece192c0a3 Mon Sep 17 00:00:00 2001 From: uvos Date: Fri, 13 Mar 2026 01:16:22 +0100 Subject: [PATCH 06/40] Revise README installation instructions Updated README to change 'Building' section to 'Installing' and reorganized instructions. --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2333021..f877bf8 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,12 @@ A C++ Qt graphical user interface for generating music using [acestep.cpp](https - CMake 3.14+ - acestep.cpp (bring your own binaries) -## Building +## Installing -### Build acestep.cpp first: +1. Grab the latest release from https://github.com/IMbackK/aceradio/releases/ +2. For now you will have to provide acestep.cpp binaries, these can be optained by following the instructions at https://github.com/ServeurpersoCom/acestep.cpp or the **Build acestep.cpp** section below + +## Build acestep.cpp: ```bash git clone https://github.com/ServeurpersoCom/acestep.cpp.git @@ -23,7 +26,7 @@ make -j$(nproc) ./models.sh # Download models (requires ~7.7 GB free space) ``` -### Build the GUI: +## Building ```bash git clone https://github.com/IMbackK/aceradio.git From 6f36719795cf84277eb8c026c50f1c630f61a379 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Tue, 17 Mar 2026 18:54:08 +0100 Subject: [PATCH 07/40] Add copywrite statements and LICENSE --- .copywrite.hcl | 15 + CMakeLists.txt | 3 + LICENSE | 674 +++++++++++++++++++++++++++++++++ README.md | 4 + src/AceStepWorker.cpp | 3 + src/AceStepWorker.h | 5 + src/AdvancedSettingsDialog.cpp | 3 + src/AdvancedSettingsDialog.h | 5 + src/AudioPlayer.cpp | 3 + src/AudioPlayer.h | 5 + src/MainWindow.cpp | 3 + src/MainWindow.h | 5 + src/SongDialog.cpp | 3 + src/SongDialog.h | 5 + src/SongItem.h | 5 + src/SongListModel.cpp | 3 + src/SongListModel.h | 5 + src/clickableslider.cpp | 3 + src/clickableslider.h | 5 + src/elidedlabel.cpp | 3 + src/elidedlabel.h | 5 + src/main.cpp | 3 + 22 files changed, 768 insertions(+) create mode 100644 .copywrite.hcl create mode 100644 LICENSE diff --git a/.copywrite.hcl b/.copywrite.hcl new file mode 100644 index 0000000..807c217 --- /dev/null +++ b/.copywrite.hcl @@ -0,0 +1,15 @@ +schema_version = 1 + +project { + license = "GPL-3.0-or-later" + copyright_year = 2026 + copyright_holder = "Carl Philipp Klemm" + + # (OPTIONAL) A list of globs that should not have copyright/license headers. + # Supports doublestar glob patterns for more flexibility in defining which + # files or folders should be ignored + header_ignore = [ + # "vendor/**", + # "**autogen**", + ] +} diff --git a/CMakeLists.txt b/CMakeLists.txt index c3dcea3..a542e75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright Carl Philipp Klemm 2026 +# SPDX-License-Identifier: GPL-3.0-or-later + cmake_minimum_required(VERSION 3.14) project(aceradio LANGUAGES CXX) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index f877bf8..4b40b46 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,7 @@ make -j$(nproc) ## Setup Paths: Go to settings->Ace Step->Model Paths and add the paths to the acestep.cpp binaries the models. + +## License + +Aceradio and all its files, unless otherwise noted, are licensed to you under the GPL-3.0-or-later license diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index e318e8f..836a904 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -1,3 +1,6 @@ +// Copyright Carl Philipp Klemm 2026 +// SPDX-License-Identifier: GPL-3.0-or-later + #include "AceStepWorker.h" #include #include diff --git a/src/AceStepWorker.h b/src/AceStepWorker.h index 41c4676..7b03da7 100644 --- a/src/AceStepWorker.h +++ b/src/AceStepWorker.h @@ -1,3 +1,8 @@ +/* + * Copyright Carl Philipp Klemm 2026 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + #ifndef ACESTEPWORKER_H #define ACESTEPWORKER_H diff --git a/src/AdvancedSettingsDialog.cpp b/src/AdvancedSettingsDialog.cpp index 163ba8a..a34dc4e 100644 --- a/src/AdvancedSettingsDialog.cpp +++ b/src/AdvancedSettingsDialog.cpp @@ -1,3 +1,6 @@ +// Copyright Carl Philipp Klemm 2026 +// SPDX-License-Identifier: GPL-3.0-or-later + #include "AdvancedSettingsDialog.h" #include "ui_AdvancedSettingsDialog.h" #include diff --git a/src/AdvancedSettingsDialog.h b/src/AdvancedSettingsDialog.h index fb4aa69..8db247c 100644 --- a/src/AdvancedSettingsDialog.h +++ b/src/AdvancedSettingsDialog.h @@ -1,3 +1,8 @@ +/* + * Copyright Carl Philipp Klemm 2026 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + #ifndef ADVANCEDSETTINGSDIALOG_H #define ADVANCEDSETTINGSDIALOG_H diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp index 76694d4..2f039bb 100644 --- a/src/AudioPlayer.cpp +++ b/src/AudioPlayer.cpp @@ -1,3 +1,6 @@ +// Copyright Carl Philipp Klemm 2026 +// SPDX-License-Identifier: GPL-3.0-or-later + #include "AudioPlayer.h" #include diff --git a/src/AudioPlayer.h b/src/AudioPlayer.h index 93f14b2..6eb4b25 100644 --- a/src/AudioPlayer.h +++ b/src/AudioPlayer.h @@ -1,3 +1,8 @@ +/* + * Copyright Carl Philipp Klemm 2026 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + #ifndef AUDIOPLAYER_H #define AUDIOPLAYER_H diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 62c8a63..d54e35a 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,3 +1,6 @@ +// Copyright Carl Philipp Klemm 2026 +// SPDX-License-Identifier: GPL-3.0-or-later + #include "MainWindow.h" #include "ui_MainWindow.h" #include "SongDialog.h" diff --git a/src/MainWindow.h b/src/MainWindow.h index 54136f7..9a7406a 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -1,3 +1,8 @@ +/* + * Copyright Carl Philipp Klemm 2026 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + #ifndef MAINWINDOW_H #define MAINWINDOW_H diff --git a/src/SongDialog.cpp b/src/SongDialog.cpp index baa9ddc..13a1fa9 100644 --- a/src/SongDialog.cpp +++ b/src/SongDialog.cpp @@ -1,3 +1,6 @@ +// Copyright Carl Philipp Klemm 2026 +// SPDX-License-Identifier: GPL-3.0-or-later + #include "SongDialog.h" #include "ui_SongDialog.h" #include diff --git a/src/SongDialog.h b/src/SongDialog.h index c8137f5..9090663 100644 --- a/src/SongDialog.h +++ b/src/SongDialog.h @@ -1,3 +1,8 @@ +/* + * Copyright Carl Philipp Klemm 2026 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + #ifndef SONGDIALOG_H #define SONGDIALOG_H diff --git a/src/SongItem.h b/src/SongItem.h index 8a28891..d64f041 100644 --- a/src/SongItem.h +++ b/src/SongItem.h @@ -1,3 +1,8 @@ +/* + * Copyright Carl Philipp Klemm 2026 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + #pragma once #include #include diff --git a/src/SongListModel.cpp b/src/SongListModel.cpp index caa856a..a77747a 100644 --- a/src/SongListModel.cpp +++ b/src/SongListModel.cpp @@ -1,3 +1,6 @@ +// Copyright Carl Philipp Klemm 2026 +// SPDX-License-Identifier: GPL-3.0-or-later + #include "SongListModel.h" #include #include diff --git a/src/SongListModel.h b/src/SongListModel.h index 4a3275d..c13ff4c 100644 --- a/src/SongListModel.h +++ b/src/SongListModel.h @@ -1,3 +1,8 @@ +/* + * Copyright Carl Philipp Klemm 2026 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + #ifndef SONGLISTMODEL_H #define SONGLISTMODEL_H diff --git a/src/clickableslider.cpp b/src/clickableslider.cpp index 3006b2e..63477aa 100644 --- a/src/clickableslider.cpp +++ b/src/clickableslider.cpp @@ -1,3 +1,6 @@ +// Copyright Carl Philipp Klemm 2026 +// SPDX-License-Identifier: GPL-3.0-or-later + #include "clickableslider.h" #include diff --git a/src/clickableslider.h b/src/clickableslider.h index 4b4e669..0d651e1 100644 --- a/src/clickableslider.h +++ b/src/clickableslider.h @@ -1,3 +1,8 @@ +/* + * Copyright Carl Philipp Klemm 2026 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + #ifndef CLICKABLESLIDER_H #define CLICKABLESLIDER_H diff --git a/src/elidedlabel.cpp b/src/elidedlabel.cpp index 191a0fd..5830ed7 100644 --- a/src/elidedlabel.cpp +++ b/src/elidedlabel.cpp @@ -1,3 +1,6 @@ +// Copyright Carl Philipp Klemm 2026 +// SPDX-License-Identifier: GPL-3.0-or-later + #include "elidedlabel.h" #include diff --git a/src/elidedlabel.h b/src/elidedlabel.h index 76df007..285a41c 100644 --- a/src/elidedlabel.h +++ b/src/elidedlabel.h @@ -1,3 +1,8 @@ +/* + * Copyright Carl Philipp Klemm 2026 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + #ifndef ELIDEDLABEL_H #define ELIDEDLABEL_H diff --git a/src/main.cpp b/src/main.cpp index c51ac1f..f401fda 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,6 @@ +// Copyright Carl Philipp Klemm 2026 +// SPDX-License-Identifier: GPL-3.0-or-later + #include #include From 6db87d119df257524939df97796820a3519b3830 Mon Sep 17 00:00:00 2001 From: unknown <16974509+cynodesmus@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:46:12 +0400 Subject: [PATCH 08/40] feat: expand vocal language combo box (24-->50+ languages) - Languages from the full list of "50+" have been added - Backward compatible with original en/zh/ja --- src/SongDialog.cpp | 52 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/SongDialog.cpp b/src/SongDialog.cpp index 13a1fa9..c63b171 100644 --- a/src/SongDialog.cpp +++ b/src/SongDialog.cpp @@ -24,14 +24,56 @@ SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &l // Setup vocal language combo box ui->vocalLanguageCombo->addItem("--", ""); // Unset ui->vocalLanguageCombo->addItem("English (en)", "en"); - ui->vocalLanguageCombo->addItem("German (de)", "de"); - ui->vocalLanguageCombo->addItem("French (fr)", "fr"); - ui->vocalLanguageCombo->addItem("Spanish (es)", "es"); - ui->vocalLanguageCombo->addItem("Japanese (ja)", "ja"); ui->vocalLanguageCombo->addItem("Chinese (zh)", "zh"); - ui->vocalLanguageCombo->addItem("Italian (it)", "it"); + ui->vocalLanguageCombo->addItem("Japanese (ja)", "ja"); + ui->vocalLanguageCombo->addItem("Korean (ko)", "ko"); + ui->vocalLanguageCombo->addItem("Spanish (es)", "es"); + ui->vocalLanguageCombo->addItem("French (fr)", "fr"); + ui->vocalLanguageCombo->addItem("German (de)", "de"); ui->vocalLanguageCombo->addItem("Portuguese (pt)", "pt"); ui->vocalLanguageCombo->addItem("Russian (ru)", "ru"); + ui->vocalLanguageCombo->addItem("Italian (it)", "it"); + ui->vocalLanguageCombo->addItem("Arabic (ar)", "ar"); + ui->vocalLanguageCombo->addItem("Azerbaijani (az)", "az"); + ui->vocalLanguageCombo->addItem("Bulgarian (bg)", "bg"); + ui->vocalLanguageCombo->addItem("Bengali (bn)", "bn"); + ui->vocalLanguageCombo->addItem("Catalan (ca)", "ca"); + ui->vocalLanguageCombo->addItem("Czech (cs)", "cs"); + ui->vocalLanguageCombo->addItem("Danish (da)", "da"); + ui->vocalLanguageCombo->addItem("Greek (el)", "el"); + ui->vocalLanguageCombo->addItem("Persian (fa)", "fa"); + ui->vocalLanguageCombo->addItem("Finnish (fi)", "fi"); + ui->vocalLanguageCombo->addItem("Hebrew (he)", "he"); + ui->vocalLanguageCombo->addItem("Hindi (hi)", "hi"); + ui->vocalLanguageCombo->addItem("Croatian (hr)", "hr"); + ui->vocalLanguageCombo->addItem("Haitian Creole (ht)", "ht"); + ui->vocalLanguageCombo->addItem("Hungarian (hu)", "hu"); + ui->vocalLanguageCombo->addItem("Indonesian (id)", "id"); + ui->vocalLanguageCombo->addItem("Icelandic (is)", "is"); + ui->vocalLanguageCombo->addItem("Latin (la)", "la"); + ui->vocalLanguageCombo->addItem("Lithuanian (lt)", "lt"); + ui->vocalLanguageCombo->addItem("Malay (ms)", "ms"); + ui->vocalLanguageCombo->addItem("Nepali (ne)", "ne"); + ui->vocalLanguageCombo->addItem("Dutch (nl)", "nl"); + ui->vocalLanguageCombo->addItem("Norwegian (no)", "no"); + ui->vocalLanguageCombo->addItem("Punjabi (pa)", "pa"); + ui->vocalLanguageCombo->addItem("Polish (pl)", "pl"); + ui->vocalLanguageCombo->addItem("Romanian (ro)", "ro"); + ui->vocalLanguageCombo->addItem("Sanskrit (sa)", "sa"); + ui->vocalLanguageCombo->addItem("Slovak (sk)", "sk"); + ui->vocalLanguageCombo->addItem("Serbian (sr)", "sr"); + ui->vocalLanguageCombo->addItem("Swedish (sv)", "sv"); + ui->vocalLanguageCombo->addItem("Swahili (sw)", "sw"); + ui->vocalLanguageCombo->addItem("Tamil (ta)", "ta"); + ui->vocalLanguageCombo->addItem("Telugu (te)", "te"); + ui->vocalLanguageCombo->addItem("Thai (th)", "th"); + ui->vocalLanguageCombo->addItem("Tagalog (tl)", "tl"); + ui->vocalLanguageCombo->addItem("Turkish (tr)", "tr"); + ui->vocalLanguageCombo->addItem("Ukrainian (uk)", "uk"); + ui->vocalLanguageCombo->addItem("Urdu (ur)", "ur"); + ui->vocalLanguageCombo->addItem("Vietnamese (vi)", "vi"); + ui->vocalLanguageCombo->addItem("Cantonese (yue)", "yue"); + ui->vocalLanguageCombo->addItem("Unknown", "unknown"); // Set current language if provided if (!vocalLanguage.isEmpty()) From 61003eb84ccdd2ae4143b09b8ad7d73152677b53 Mon Sep 17 00:00:00 2001 From: unknown <16974509+cynodesmus@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:46:12 +0400 Subject: [PATCH 09/40] fix: correct typos in comments, error messages, variables - Fixed spelling in comments throughout codebase - Corrected error messages and user-facing strings - Standardized variable names (cancleGenerateion --> cancelGeneration where needed) - No functional changes, purely cosmetic fixes --- src/AceStepWorker.cpp | 12 ++++++------ src/AceStepWorker.h | 6 +++--- src/MainWindow.cpp | 8 ++++---- src/SongItem.h | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 836a904..fc999f2 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -18,14 +18,14 @@ AceStep::AceStep(QObject* parent): QObject(parent) connect(&ditVaeProcess, &QProcess::finished, this, &AceStep::ditProcFinished); } -bool AceStep::isGenerateing(SongItem* song) +bool AceStep::isGenerating(SongItem* song) { if(!busy && song) *song = this->request.song; return busy; } -void AceStep::cancleGenerateion() +void AceStep::cancelGeneration() { qwenProcess.blockSignals(true); qwenProcess.terminate(); @@ -39,7 +39,7 @@ void AceStep::cancleGenerateion() progressUpdate(100); if(busy) - generationCancled(request.song); + generationCanceled(request.song); busy = false; } @@ -50,7 +50,7 @@ bool AceStep::requestGeneration(SongItem song, QString requestTemplate, QString { if(busy) { - qWarning()<<"Droping song:"<progressBar, &QProgressBar::setValue); @@ -102,7 +102,7 @@ MainWindow::MainWindow(QWidget *parent) MainWindow::~MainWindow() { - aceStep->cancleGenerateion(); + aceStep->cancelGeneration(); autoSavePlaylist(); saveSettings(); @@ -523,7 +523,7 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent) SongItem lastSong; SongItem workerSong; - if(aceStep->isGenerateing(&workerSong)) + if(aceStep->isGenerating(&workerSong)) lastSong = workerSong; else if(!generatedSongQueue.empty()) lastSong = generatedSongQueue.last(); @@ -553,7 +553,7 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent) void MainWindow::flushGenerationQueue() { generatedSongQueue.clear(); - aceStep->cancleGenerateion(); + aceStep->cancelGeneration(); isGeneratingNext = false; } diff --git a/src/SongItem.h b/src/SongItem.h index d64f041..71f7af8 100644 --- a/src/SongItem.h +++ b/src/SongItem.h @@ -21,7 +21,7 @@ public: inline SongItem(const QString &caption = "", const QString &lyrics = "") : caption(caption), lyrics(lyrics) { - // Generate a unique ID using cryptographically secure random number + // Generate a unique ID using a cryptographically secure random number uniqueId = QRandomGenerator::global()->generate64(); } }; From 6297477e29c943e4c4d6ac7d0d56f56339016c14 Mon Sep 17 00:00:00 2001 From: unknown <16974509+cynodesmus@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:35:01 +0400 Subject: [PATCH 10/40] feat: restore WAV as default output (aceradio compatibility) acestep.cpp previously generated *.wav by default for maximum quality. Later changed to mp3, but aceradio expects WAV input. - Revert to WAV default to match aceradio workflow - No conversion overhead or compatibility issues - Preserves original high-quality generation logic --- src/AceStepWorker.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index fc999f2..1528805 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -181,6 +181,7 @@ void AceStep::qwenProcFinished(int code, QProcess::ExitStatus status) ditVaeArgs << "--text-encoder"< Date: Mon, 16 Mar 2026 23:37:30 +0400 Subject: [PATCH 11/40] feat: rename binaries ace-qwen3 --> ace-lm, dit-vae --> ace-synth (1e627bf+) In acestep.cpp since commit 1e627bf, binary file names changed: - ace-qwen3 --> ace-lm - dit-vae --> ace-synth ! For users of previous aceradio versions: - Delete settings for re-configuration - Binaries rebuild with new names --- src/AceStepWorker.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 1528805..6f4bbe8 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -57,11 +57,11 @@ bool AceStep::requestGeneration(SongItem song, QString requestTemplate, QString request = {song, QRandomGenerator::global()->generate(), aceStepPath, textEncoderModelPath, ditModelPath, vaeModelPath}; - QString qwen3Binary = aceStepPath + "/ace-qwen3"; + QString qwen3Binary = aceStepPath + "/ace-lm"; QFileInfo qwen3Info(qwen3Binary); if (!qwen3Info.exists() || !qwen3Info.isExecutable()) { - generationError("ace-qwen3 binary not found at: " + qwen3Binary); + generationError("ace-lm binary not found at: " + qwen3Binary); busy = false; return false; } @@ -136,16 +136,16 @@ void AceStep::qwenProcFinished(int code, QProcess::ExitStatus status) if(code != 0) { QString errorOutput = qwenProcess.readAllStandardError(); - generationError("ace-qwen3 exited with code " + QString::number(code) + ": " + errorOutput); + generationError("ace-lm exited with code " + QString::number(code) + ": " + errorOutput); busy = false; return; } - QString ditVaeBinary = request.aceStepPath + "/dit-vae"; + QString ditVaeBinary = request.aceStepPath + "/ace-synth"; QFileInfo ditVaeInfo(ditVaeBinary); if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable()) { - generationError("dit-vae binary not found at: " + ditVaeBinary); + generationError("ace-synth binary not found at: " + ditVaeBinary); busy = false; return; } @@ -153,7 +153,7 @@ void AceStep::qwenProcFinished(int code, QProcess::ExitStatus status) request.requestLlmFilePath = tempDir + "/request_" + QString::number(request.uid) + "0.json"; if (!QFileInfo::exists(request.requestLlmFilePath)) { - generationError("ace-qwen3 failed to create enhanced request file "+request.requestLlmFilePath); + generationError("ace-lm failed to create enhanced request file "+request.requestLlmFilePath); busy = false; return; } @@ -175,7 +175,7 @@ void AceStep::qwenProcFinished(int code, QProcess::ExitStatus status) } } - // Step 2: Run dit-vae to generate audio + // Step 2: Run ace-synth to generate audio QStringList ditVaeArgs; ditVaeArgs << "--request"< Date: Tue, 17 Mar 2026 18:37:20 +0400 Subject: [PATCH 12/40] feat: add Windows compatibility for external process execution This PR introduces cross-platform support for launching external binaries (`ace-lm` and `ace-synth`) on Windows systems. **Key changes:** * Introduced `EXE_EXT` constant in `AceStepWorker.h` using `Q_OS_WIN` macro to handle `.exe` extensions automatically. * Updated binary path construction to ensure `QFileInfo::isExecutable()` and `QProcess` work correctly on Windows. * The code remains fully compatible with Linux/macOS (extension remains empty). **Why this is needed:** On Windows, `QProcess` and `QFileInfo` require the `.exe` suffix to correctly identify and execute binary files. Without this change, the application fails to find the required tools even if they are present in the directory. --- README.md | 24 ++++++++++++++++++++++++ src/AceStepWorker.cpp | 4 ++-- src/AceStepWorker.h | 6 ++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b40b46..150700f 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ make -j$(nproc) ## Building +### Linux / macOS ```bash git clone https://github.com/IMbackK/aceradio.git cd aceradio @@ -36,6 +37,29 @@ cmake .. make -j$(nproc) ``` +### Windows (Qt6 + CMake) + +To build on Windows, ensure you have **CMake** and **Qt6** installed. Run the following commands in **Command Prompt (cmd)** or **PowerShell**: + +1. **Configure and Build:** +```cmd +git clone https://github.com/IMbackK/aceradio.git +cd aceradio +mkdir build +cmake -B build -DCMAKE_PREFIX_PATH="C:/Qt/6.x.x/msvc2019_64" + +# Use --parallel to build using all CPU cores +# Or use -j N (e.g., -j 4) to limit the number of threads +cmake --build build --config Release --parallel +``` +Note: Replace C:/Qt/6.x.x/msvc2019_64 with your actual Qt installation path. + +2. **Deploy Dependencies:** +Run windeployqt to copy necessary Qt libraries to the build folder: +```cmd +"C:\Qt\6.x.x\msvc2019_64\bin\windeployqt.exe" --no-translations build\Release\aceradio.exe +``` + ## Setup Paths: Go to settings->Ace Step->Model Paths and add the paths to the acestep.cpp binaries the models. diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 6f4bbe8..6dce972 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -57,7 +57,7 @@ bool AceStep::requestGeneration(SongItem song, QString requestTemplate, QString request = {song, QRandomGenerator::global()->generate(), aceStepPath, textEncoderModelPath, ditModelPath, vaeModelPath}; - QString qwen3Binary = aceStepPath + "/ace-lm"; + QString qwen3Binary = aceStepPath + "/ace-lm" + EXE_EXT; QFileInfo qwen3Info(qwen3Binary); if (!qwen3Info.exists() || !qwen3Info.isExecutable()) { @@ -141,7 +141,7 @@ void AceStep::qwenProcFinished(int code, QProcess::ExitStatus status) return; } - QString ditVaeBinary = request.aceStepPath + "/ace-synth"; + QString ditVaeBinary = request.aceStepPath + "/ace-synth" + EXE_EXT; QFileInfo ditVaeInfo(ditVaeBinary); if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable()) { diff --git a/src/AceStepWorker.h b/src/AceStepWorker.h index bcdf6b5..17962f8 100644 --- a/src/AceStepWorker.h +++ b/src/AceStepWorker.h @@ -13,6 +13,12 @@ #include "SongItem.h" +#ifdef Q_OS_WIN +inline const QString EXE_EXT = ".exe"; +#else +inline const QString EXE_EXT = ""; +#endif + class AceStep : public QObject { Q_OBJECT From 2de2d1bc6bf1444729710a88f2f51a7e6c16c2ea Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Tue, 17 Mar 2026 19:00:22 +0100 Subject: [PATCH 13/40] Revise windows section in readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 150700f..b5e9d35 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ make -j$(nproc) ### Windows (Qt6 + CMake) -To build on Windows, ensure you have **CMake** and **Qt6** installed. Run the following commands in **Command Prompt (cmd)** or **PowerShell**: +To build on Windows run the following commands in *Command Prompt (cmd)* or *PowerShell*: -1. **Configure and Build:** +1. Configure and Build: ```cmd git clone https://github.com/IMbackK/aceradio.git cd aceradio @@ -54,7 +54,7 @@ cmake --build build --config Release --parallel ``` Note: Replace C:/Qt/6.x.x/msvc2019_64 with your actual Qt installation path. -2. **Deploy Dependencies:** +2. Deploy Dependencies: Run windeployqt to copy necessary Qt libraries to the build folder: ```cmd "C:\Qt\6.x.x\msvc2019_64\bin\windeployqt.exe" --no-translations build\Release\aceradio.exe From 970ed468928e2ec3dde592933aec36510b6d38d3 Mon Sep 17 00:00:00 2001 From: cynodesmus <16974509+cynodesmus@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:37:37 +0400 Subject: [PATCH 14/40] feat: The syntax for running models has been changed In acestep.cpp since commit 9567a45, rename CLI flags across all binaries: --model -> --lm, --text-encoder -> --embedding --- src/AceStepWorker.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 6dce972..7626070 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -122,7 +122,7 @@ bool AceStep::requestGeneration(SongItem song, QString requestTemplate, QString QStringList qwen3Args; qwen3Args << "--request" << request.requestFilePath; - qwen3Args << "--model" << qwen3ModelPath; + qwen3Args << "--lm" << qwen3ModelPath; progressUpdate(30); @@ -178,7 +178,7 @@ void AceStep::qwenProcFinished(int code, QProcess::ExitStatus status) // Step 2: Run ace-synth to generate audio QStringList ditVaeArgs; ditVaeArgs << "--request"< Date: Fri, 20 Mar 2026 10:09:25 +0100 Subject: [PATCH 15/40] Add ability to enable and disable cot per song to ui --- src/AceStepWorker.cpp | 2 + src/MainWindow.cpp | 18 +--- src/SongDialog.cpp | 20 ++-- src/SongDialog.h | 3 +- src/SongDialog.ui | 219 +++++++++++++++++++++--------------------- src/SongItem.h | 3 +- 6 files changed, 131 insertions(+), 134 deletions(-) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 7626070..837247b 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -109,6 +109,8 @@ bool AceStep::requestGeneration(SongItem song, QString requestTemplate, QString if (!song.vocalLanguage.isEmpty()) requestObj["vocal_language"] = song.vocalLanguage; + requestObj["use_cot_caption"] = song.cotCaption; + // Write the request file QFile requestFileHandle(request.requestFilePath); if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text)) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index e79bb47..d2dd51d 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -698,7 +698,8 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList(song.uniqueId); // Store as qint64 for JSON compatibility + songObj["uniqueId"] = static_cast(song.uniqueId); + songObj["use_cot_caption"] = song.cotCaption; songsArray.append(songObj); } @@ -779,31 +780,20 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList & SongItem song; if (songObj.contains("caption")) - { song.caption = songObj["caption"].toString(); - } if (songObj.contains("lyrics")) - { song.lyrics = songObj["lyrics"].toString(); - } - // Load vocalLanguage if present if (songObj.contains("vocalLanguage")) - { song.vocalLanguage = songObj["vocalLanguage"].toString(); - } - // Load uniqueId if present (for backward compatibility) if (songObj.contains("uniqueId")) - { song.uniqueId = static_cast(songObj["uniqueId"].toInteger()); - } else - { - // Generate new ID for old playlists without uniqueId song.uniqueId = QRandomGenerator::global()->generate64(); - } + + song.cotCaption = songObj["use_cot_caption"].toBool(true); songs.append(song); } diff --git a/src/SongDialog.cpp b/src/SongDialog.cpp index c63b171..6f5db6b 100644 --- a/src/SongDialog.cpp +++ b/src/SongDialog.cpp @@ -5,21 +5,16 @@ #include "ui_SongDialog.h" #include -SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &lyrics, const QString &vocalLanguage) +SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &lyrics, const QString &vocalLanguage, bool cotEnabled) : QDialog(parent), ui(new Ui::SongDialog) { ui->setupUi(this); - // Set initial values if provided - if (!caption.isEmpty()) - { - ui->captionEdit->setPlainText(caption); - } - if (!lyrics.isEmpty()) - { - ui->lyricsEdit->setPlainText(lyrics); - } + ui->captionEdit->setPlainText(caption); + ui->lyricsEdit->setPlainText(lyrics); + + ui->checkBoxEnhanceCaption->setChecked(cotEnabled); // Setup vocal language combo box ui->vocalLanguageCombo->addItem("--", ""); // Unset @@ -110,6 +105,11 @@ QString SongDialog::getVocalLanguage() const return ui->vocalLanguageCombo->currentData().toString(); } +bool SongDialog::getCotEnabled() const +{ + return ui->checkBoxEnhanceCaption->isChecked(); +} + void SongDialog::on_okButton_clicked() { // Validate that caption is not empty diff --git a/src/SongDialog.h b/src/SongDialog.h index 9090663..7230814 100644 --- a/src/SongDialog.h +++ b/src/SongDialog.h @@ -20,12 +20,13 @@ class SongDialog : public QDialog public: explicit SongDialog(QWidget *parent = nullptr, const QString &caption = "", const QString &lyrics = "", - const QString &vocalLanguage = ""); + const QString &vocalLanguage = "", bool cotEnabled = true); ~SongDialog(); QString getCaption() const; QString getLyrics() const; QString getVocalLanguage() const; + bool getCotEnabled() const; private slots: void on_okButton_clicked(); diff --git a/src/SongDialog.ui b/src/SongDialog.ui index efedda8..8c0710a 100644 --- a/src/SongDialog.ui +++ b/src/SongDialog.ui @@ -1,109 +1,112 @@ - - SongDialog - - - - 0 - 0 - 500 - 400 - - - - Song Details - - - - - - Caption: - - - Qt::AlignTop - - - - - - - Enter song caption (e.g., "Upbeat pop rock anthem with driving electric guitars") - - - 80 - - - - - - - Lyrics (optional): - - - Qt::AlignTop - - - - - - - Enter lyrics or leave empty for instrumental music - - - 150 - - - - - - - Vocal Language: - - - Qt::AlignTop - - - - - - - true - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - OK - - - - - - - Cancel - - - - - - - - - - \ No newline at end of file + + SongDialog + + + + 0 + 0 + 500 + 410 + + + + Song Details + + + + + + 0 + + + + + Caption: + + + Qt::AlignmentFlag::AlignTop + + + + + + + Enhance Caption + + + true + + + + + + + + + + + + Lyrics (optional): + + + Qt::AlignmentFlag::AlignTop + + + + + + + + + + Vocal Language: + + + Qt::AlignmentFlag::AlignTop + + + + + + + true + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + OK + + + + + + + Cancel + + + + + + + + + + diff --git a/src/SongItem.h b/src/SongItem.h index 71f7af8..7084e0f 100644 --- a/src/SongItem.h +++ b/src/SongItem.h @@ -16,10 +16,11 @@ public: uint64_t uniqueId; QString file; QString vocalLanguage; + bool cotCaption; QString json; inline SongItem(const QString &caption = "", const QString &lyrics = "") - : caption(caption), lyrics(lyrics) + : caption(caption), lyrics(lyrics), cotCaption(true) { // Generate a unique ID using a cryptographically secure random number uniqueId = QRandomGenerator::global()->generate64(); From 64357be451573be4da7128d55ad6f943d52cc53b Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Sat, 21 Mar 2026 23:09:06 +0100 Subject: [PATCH 16/40] Icon: make card opaque --- res/xyz.uvos.aceradio.png | Bin 3747 -> 3626 bytes res/xyz.uvos.aceradio.svg | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/res/xyz.uvos.aceradio.png b/res/xyz.uvos.aceradio.png index 603d014365b2ee8d9f2d6a42793782dfe4ac037f..032045e18afa712fe3969fe82388ac862ed33d0d 100644 GIT binary patch delta 3625 zcmV+^4%YFb9jY9VB!3BTNLh0L00EZ(00EZ)bF)nW0000PbVXQnQ*UN;cVTj60C#tH zE@^ISb7Ns}WiD@WXPfRk8UO$dw@E}nRCt`lTX|SiSC;?1R|`}X#ZCbNme5MXh|1Dz z!Jw^K#MYQNZs|<&eKY#SU`&EGf&?wVB_@1|xWt6EwG-2EA%B@9CMM}<&^V5hAc`om z?_0UBQ$?-sz4@cCB1>(rDChTAy?f7gPu+L#x#yloFbo4n%5fdY@h1E*a2ovyaRUc$oY?w>t#DD z?o|8JG(BN*r%Vn~MMlgzBX)CtQznxsCQ9yvIUqALGr6Xw=A)bC6+w=Ct$ckY`lO_U z!+YNQAQgb!itKhkQBe;6;-!1%E?l^@P_Nh9Yme2K`F~_s_mgvGKlfg4?sa?Q*yVs- zyK)k)UcL51RdubaHS`>k#N?nrwX3`9AfLxma{1f=lH}4_l}fDAXhbTN+NHDeu8T^g zvP!>LET-orCG03DIGD|L46GfHk&*H0zka%KyuW{dG)L#|?oNk=O{)r;n3Y>Wdy?A$3^d-GP+w(_!DL6n(Ab#6l3l}}E5 zI@e|ewmKj;E9);sXTLk4R1S?hcu!AvIzBG;?|*&sQ2aZzvggNBaI&ZR>`sxf~=TYGycggo*3%TNqZ!(&(A7mK9Pt+_hKBgo@F^8aCkB8*q4c;@S?xC- z%@k>nIw|RCFPTg>WJw;^8l@Q-?=HS{`G0@L9XtTEwzhLyTid5wBkOUXxuwlHq@}qC zfW@7`su{3k$&&kbDrQ&2TZ`+o8+ng+mGazmA=C{ThJOFG-+l+Zhe=n(9rx308!@n%?2?5P+Q-SX{yiD)6@97tE?0a@9#$I zw_jQkIiYZN7J$ng|9`wzdNU$DJw4DIzgY+D*s(qSPGz;PseV5Hcf}oFe+jQe$+-8! zIrA1f0ECHfOr=s2g9Cke=6KCIpntRTe&*;SEuYUrNXV3a0N`+V004&4W8iiLHoJ0w%9#kh=`bVd~{oVLz8&x)=g7Q@tAZ#e*Uh2rlwYx(d{8qf`30GNYb!% z49~revtMCqMiwIT4?~)=5%7#tk30U=Iia6lF-qad`BZc}Y)Q5{pF)V0bm+VxrHRVl}e}7G$}(yY@bO z_V)H!nwPil6aY{ZJ;DJux1~eX*a!g7$mO{H)_O3ML(};q!7!jG8U#TQGTENiW5vUGpnZ@vKv}n;H)OB|Vra+xy+{__@Ew7c%u5tMG)@cRqQ5(Eqmkbn3|E+!-->R)(aSxJyTM0qBAEE6%_?dUk|P>UjnUM4pJmS z{1<28A`Lccw7mE{ZomBo7>Y*Ff3Cxf&0DP+?0ATb{3H7M`%U&cIq{M3RLt$*Y13C^ zZP`-uXy1q#aDQyy`?F7;I(6^+@6RV&8vFWcmCJrmR4srN<72o zAhzf$xK4e-yfHmpxRJ6BwDOVLErvl&&UT2T!SITRwtro@sngIX^!4?Vr@uJ+2?a{M zef!QL;E@?nQd07>t%Dz}*3#f z+qQFb+Vi-&(YP`VX{y zg+7^l$%~N?8U&qKm^&_-EV%= zW(kI(aQ&^

A$rErZr*P`+scG(BAtXxZ=3sx=-Y4cf~Q!W&P0ih+{f?3SaI-$ix7 zUb8d=n?(Qs#ehdTaY?I+8*GL2rPUBlxBs$;FOeX0!#iea*w#2O49z1cl56p9fDO0; z0e_-CJPMw-H)|QBz!~8OjzAzWnVAP*#|)ro9tk_@dMtTf=r;+o4;%sMWVXm-d=Lbt zWo5&ARs>7yEQ0|9lHhRIR&QA4d9OK92+i4H^JX2mb{(d?xY83otIuvvV>lYggR zX8r*Xqnti9Jrlk$mS2(Pug;*e_$Qn7js}m%gS)%a=Oh3G2TL#7Ezt(}#wB8E#=FL6 zztyV|vgTEbe7zO7P?wvH$}OqrtE#Zeh8ba@4|eU^e~$zJPY+S5NGNpthXaowWX-FX z^ztiki;KgocXKV$Yy10gGj#)Kjei={kP2mQz79=qH*5W2Pdu>?7|sX0_g;SUQ*-0K zVJ(d%W@ct1`tVT@9Lu#@Wf|{4+tFcs*2?85OEVuLo5Jky8S>qG_Ur*3$qD-V`Y(^1 z6K+#NjwA?)omxW>d9pCvCXN64DjY?2W=?GFPdIx{h+Po>*%#pO4L4y?(J|=l9a9}+gj5=+ z3ZFIew3mn1I+;v1;_zd(PJeLk-W>1l?x92{CtgTPYsc!=mi8I`0h8*wA9Mv**VMV4 z`uqf9Vq;D6I|74^b>NC!A@KSAgcunWgTB5#EL)cRS^3S1WnNyMJyL1FKgB}VJKnzT zzon$4)|-;Ys$72Q3oqQOsHl=Wt{FhY z#OCBCD+h2noC&zowjLa3XGHEh1d?yaEA-dYfaLQrEp;>8hIa=Jhh6!tRmIX{n7AB0 z@Yp7RL8r(C?ggOJ6j$>-BmNB)hyI%SDi+ z0J~CAtiTlr;1&}DeRns!W*ctv7>0pPXLuu7$z;PM6pG{k!hg=4`xdcrv8=PQY%nu> zH)xHqcnv@O^dt0oJw!sG#tfTDuyQ~^fMmOiNHqVSCyR)(Teo#nf&#~W!32QT>8$b$ z0|rXv*zbRgAV~&~OmT*0pj0YJwMIkK)iv@7f%feq2}I vC|0XkIe$VR5+aG{g$t4nz@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B00(qQO+^Rl0|gfZ5{1bb9{>Oi z`AI}URCwCen|WMRN1nicuikro(M@wBc!7c!ASiMO%@GwaQD9e-Xg03KnTbcT_%X@G zWMuv_TAcX}5qnnyrGF+0BEJ=704|EKtP-s*ft7IjKbMm~63Zl(H zNTbW;y5_6Vd^u*!$kVG5#|p(`wp$ ze0)9$4#x6Q z|Iq;`EG$TW?rp@O5`j?dKN4s|In{oSg{amLYKBm_jVh5T_V4(l&=c>-G_Zon# ztgN}W>l=$PCW?M+^|GH<`&L3i++R0sdQWs0^umP;Qg7EcJg?Uq9+f2A=X}R%HCuoA zyA7*yX3wJ@1yG>VJ@jpJ>tRX4%HG<2RLZ6iBSKy*n4hFr5X3k7*tgimy{IF}lnl;ZVDk{#NX)_q7 z_MphJQgl?r+Rr{a@&|83fb?|TG`-$%P!Pp2_qNGpyf}606y3gk`>Tc(Kw4V*I}MG^ z&-I~tw}OHL|3L_GA;eOSV<|!mMGQl+Q=kmSz#;?y07#MmLAzyeEZaT-h5Yh!K{?Y`1Mw+Ew_g$$Ysv7)EN`E@A#nr$Ec#cVcPd$fgy1Z}OXJiW5N=Lb}2Q4snJD;l+rvGa3T zR;rFV>|3zD?FnzH`hOHXZ()FO_10!InU_8F)Y4$L0p#ZwjCVMlvwNrw!%zSKgizOu ziPiE1>mTp|6@|s#ws)5#;oj~Gf+(-8y|LVF0L{(KPu_c^Rr{#yA;E#C0RSka5&*y# zE0%Hqpp!+6T?d4u9?!`N_a*>>2?hWf;IFN?x6Nv`J>fP0Ns3|FbpIl#Fofpys4XZ5DIm5 zw`LC+z}2hOGbKstrv6H$!j_RS^Ycz0AYqA?rX{Wbpbrz9EhneqycH0W4Ty;^%S?I8 zn-Ca=fz6x$=u#-;C+=;tSga|1J;(LcV6)k$_DnNnXJ2}0y=!{n4AO2QnC70kxD^1l z0sx$RG+&dw|Ij>dJ3&f;-ib`=3bWbzuXnd&OyY;E0S2RKRL`6rT?PPP5n?)gL|2jW zSWafN@I&?YWf{ro-Uo0esnTeCF7nMo!6sBg>B+^ekT9`ViDH? zfC2yaM%U+T3d`qWkOq?+O{UHOmc;Wm( z0H&t%dew^MFIS#Ebq)Z4lJ=u0Ar$_xcp)P-)bF5zFsUPSgh?&UdrmHSjPs@mP)cFj z`neAKG1yL`i@x88Ou3=%^4j)VvPZOL+HmrW5O@BHkn`5}jj;(~4y z5`+v_ZSA9jnf$5M%Vom_;@pKR=&tzhY!8tDP5bt#hYaBL4X<)M&-ryI7^J18@0mO$ z)}Yhrj)kBS4uhhDC&t$Wtt6oLk-jn>vwpDW8q9)*pD z^B_qfz_J!LC@3lCWaW4LJy@1LV;(jdN`MfA)ao~KtFHXQ-PK1$Pts$I1Nv%Lsg#nB zO1Vd=RQ|lY{N$}Z0jw*WA92CvEV)*5BW|$8;o6$&a{vIPiLqJ4=_o^feoJyI&hP7Y z@!2zDacj#dz__clhJ1aO<(|3pXEy<~VvGX^tpBlWo7Pvo@cj96CwrK{ym^mkPt?^P z8N%YbN$&Zt%K4F_5(irRiFtEENSpr9-HUHOOj);fymHs+^{x5T7uG_cvQ$4=~ljepuJYfP|+9mBBdSWcG*>(e^L)ua5a;-ZW zKXHjCH^%7{bm-9GBcm|J9(h4oW^xcQn|pZtAwbx6G+|PTCp4hFy5$p%+YO zq5>H)nhsD&ig0@nN)=QTi%U}zJaV|gc6%tY`)d6?QmAY*uBI+AV;D|4VT?lg+5mRw zmIe@>=aJQDQhEY_X3p##?AXX}?sptCBFv!9%zVv5=SYNrvV7Y zPpQZ12tWb(`)fU#{psG?bW$FlP}KR2?_fodk+nVJoy{AEDH``xM3EVB& z+joAzjT^Ulu!G8`&-h~OGpnERWFiCrfEa|4OmMV<7bLK-;5p{MAuE$25$hE_H>aXkB(ZzC=}KGczmlSJF75$@v;_g%t>O|S`-&I zi-AGi@^5~^q?a={ZYdeWsd80mT4Le-`T(BiNKDM6Q>de6b^yF6xs?}> zA|KTYU8Sn9@Q-HgEq;5zvqeTyHnG{>BWBa?V_B)w?q~mlg90nIZTqkp0RT*yJmEuy zLT>jegsLl>kjD3Y$mO;IUSj| ze)SqoPfK1H6y*PB_z{ZFjHLC9T+#K_H=|PR%sqQ)OUE`0ih-}M`pfa-#%3P=g?HN(NPf(Yqgqkp67b~iweuKUI&oAOU0<4Dh=a88Le8Pk;LFDTfb#dSlQkyk^a_{&hdpjE@=_7FzZFwTGLUTeCG9^%;(3 zCp9*;qyd1gwx+tmn;^=vQeuHYrksaY#9VTeScQW3Prx>JoX7r1jKhtpZAu0Ao1_!mA((A5efWhG7_v zWnrj`cN+kUkmyYSbB=s&myL^D3M^-56bc!2xe|cq0r_fwkyH8Q6<`>|%Xs%rgzh|q z3Bkh=28SXK9r)D3$oNmN(eN^{n4Sd5m86^yeY|55jEr{=fLp)fAcRnO0K#zme!wIK zDOLN4DA;w|cjJz2;5g2`$mEs=5d2^O$Ri_;03lIC5MD)U&AvMuLS=|$iPr#dck%=$o?OPeaDe#iZC3p>hTqZLC07OMaJn3yMKx&^=$j7Ja zm!D;SKU64T%s2({wpY&c004P;dC}Kz+`J%)F7=ej(O1Whi!jkn0qFSE8U-o{K&3mi z7K(BE_1%A@rnGxai{k)sTvv8S0Dy8FNQ45$7$~KTD2iZrI6<#(Lyb+X(+MG5badpB z(xaujyi-a6fJf)d*45jc?>9BK#t=f_K?%#UCY4h0+~vz(?r`V%{{s@g4j6b#xI+K{ N002ovPDHLkV1j;o63_qu diff --git a/res/xyz.uvos.aceradio.svg b/res/xyz.uvos.aceradio.svg index 23516fd..70a6ec5 100644 --- a/res/xyz.uvos.aceradio.svg +++ b/res/xyz.uvos.aceradio.svg @@ -7,7 +7,7 @@ width="594" height="594" viewBox="0 0 594 594" - sodipodi:docname="icon.svg" + sodipodi:docname="xyz.uvos.aceradio.svg" inkscape:export-filename="icon64.png" inkscape:export-xdpi="10.343434" inkscape:export-ydpi="10.343434" @@ -28,9 +28,9 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" showguides="true" - inkscape:zoom="0.58212013" - inkscape:cx="-146.01797" - inkscape:cy="490.44859" + inkscape:zoom="0.82324218" + inkscape:cx="38.8707" + inkscape:cy="407.535" inkscape:window-width="1920" inkscape:window-height="1054" inkscape:window-x="0" @@ -45,6 +45,14 @@ + Date: Sat, 21 Mar 2026 23:09:39 +0100 Subject: [PATCH 17/40] Refactor json handling for SongItems and add new fields. --- CMakeLists.txt | 2 + src/AceStepWorker.cpp | 9 +--- src/MainWindow.cpp | 49 +++---------------- src/SongDialog.cpp | 111 +++++++++++++++++++++++++++++------------- src/SongDialog.h | 11 ++--- src/SongDialog.ui | 67 ++++++++++++++++++++----- src/SongItem.cpp | 43 ++++++++++++++++ src/SongItem.h | 19 +++++--- src/SongListModel.cpp | 20 ++++++++ src/SongListModel.h | 1 + 10 files changed, 224 insertions(+), 108 deletions(-) create mode 100644 src/SongItem.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a542e75..191c922 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,8 @@ add_executable(${PROJECT_NAME} ${MusicGeneratorGUI_H} res/resources.qrc src/elidedlabel.h src/elidedlabel.cpp + src/SongItem.cpp + ) # UI file diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 837247b..50d940c 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -102,14 +102,7 @@ bool AceStep::requestGeneration(SongItem song, QString requestTemplate, QString } QJsonObject requestObj = templateDoc.object(); - requestObj["caption"] = song.caption; - - if (!song.lyrics.isEmpty()) - requestObj["lyrics"] = song.lyrics; - if (!song.vocalLanguage.isEmpty()) - requestObj["vocal_language"] = song.vocalLanguage; - - requestObj["use_cot_caption"] = song.cotCaption; + song.store(requestObj); // Write the request file QFile requestFileHandle(request.requestFilePath); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index d2dd51d..d09e473 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -291,12 +291,7 @@ void MainWindow::on_addSongButton_clicked() if (dialog.exec() == QDialog::Accepted) { - QString caption = dialog.getCaption(); - QString lyrics = dialog.getLyrics(); - QString vocalLanguage = dialog.getVocalLanguage(); - - SongItem newSong(caption, lyrics); - newSong.vocalLanguage = vocalLanguage; + SongItem newSong = dialog.getSong(); songModel->addSong(newSong); // Select the new item @@ -335,18 +330,10 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index) { SongItem song = songModel->getSong(row); - SongDialog dialog(this, song.caption, song.lyrics, song.vocalLanguage); + SongDialog dialog(this, song); if (dialog.exec() == QDialog::Accepted) - { - QString caption = dialog.getCaption(); - QString lyrics = dialog.getLyrics(); - QString vocalLanguage = dialog.getVocalLanguage(); - - songModel->setData(songModel->index(row, 1), caption, SongListModel::CaptionRole); - songModel->setData(songModel->index(row, 1), lyrics, SongListModel::LyricsRole); - songModel->setData(songModel->index(row, 1), vocalLanguage, SongListModel::VocalLanguageRole); - } + songModel->updateSong(songModel->index(row, 1), dialog.getSong()); } connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); @@ -695,17 +682,13 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList(song.uniqueId); - songObj["use_cot_caption"] = song.cotCaption; + song.store(songObj); songsArray.append(songObj); } QJsonObject rootObj; rootObj["songs"] = songsArray; - rootObj["version"] = "1.0"; + rootObj["version"] = "1.1"; QJsonDocument doc(rootObj); QByteArray jsonData = doc.toJson(); @@ -754,8 +737,7 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList & QJsonObject rootObj = doc.object(); - // Check for version compatibility - if (rootObj.contains("version") && rootObj["version"].toString() != "1.0") + if (rootObj.contains("version") && rootObj["version"].toString() != "1.0" && rootObj["version"].toString() != "1.1") { qWarning() << "Unsupported playlist version:" << rootObj["version"].toString(); return false; @@ -777,24 +759,7 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList & continue; QJsonObject songObj = value.toObject(); - SongItem song; - - if (songObj.contains("caption")) - song.caption = songObj["caption"].toString(); - - if (songObj.contains("lyrics")) - song.lyrics = songObj["lyrics"].toString(); - - if (songObj.contains("vocalLanguage")) - song.vocalLanguage = songObj["vocalLanguage"].toString(); - - if (songObj.contains("uniqueId")) - song.uniqueId = static_cast(songObj["uniqueId"].toInteger()); - else - song.uniqueId = QRandomGenerator::global()->generate64(); - - song.cotCaption = songObj["use_cot_caption"].toBool(true); - + SongItem song(songObj); songs.append(song); } diff --git a/src/SongDialog.cpp b/src/SongDialog.cpp index 6f5db6b..58f4ec6 100644 --- a/src/SongDialog.cpp +++ b/src/SongDialog.cpp @@ -5,19 +5,19 @@ #include "ui_SongDialog.h" #include -SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &lyrics, const QString &vocalLanguage, bool cotEnabled) +SongDialog::SongDialog(QWidget *parent, const SongItem &song) : QDialog(parent), - ui(new Ui::SongDialog) + song(song), + ui(new Ui::SongDialog) { ui->setupUi(this); - ui->captionEdit->setPlainText(caption); - ui->lyricsEdit->setPlainText(lyrics); - - ui->checkBoxEnhanceCaption->setChecked(cotEnabled); + ui->captionEdit->setPlainText(song.caption); + ui->lyricsEdit->setPlainText(song.lyrics); + ui->checkBoxEnhanceCaption->setChecked(song.cotCaption); // Setup vocal language combo box - ui->vocalLanguageCombo->addItem("--", ""); // Unset + ui->vocalLanguageCombo->addItem("--", ""); ui->vocalLanguageCombo->addItem("English (en)", "en"); ui->vocalLanguageCombo->addItem("Chinese (zh)", "zh"); ui->vocalLanguageCombo->addItem("Japanese (ja)", "ja"); @@ -70,19 +70,69 @@ SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &l ui->vocalLanguageCombo->addItem("Cantonese (yue)", "yue"); ui->vocalLanguageCombo->addItem("Unknown", "unknown"); - // Set current language if provided - if (!vocalLanguage.isEmpty()) + ui->keyScaleCombo->addItem("--"); + ui->keyScaleCombo->addItem("C major"); + ui->keyScaleCombo->addItem("C# major"); + ui->keyScaleCombo->addItem("D major"); + ui->keyScaleCombo->addItem("D# major"); + ui->keyScaleCombo->addItem("E major"); + ui->keyScaleCombo->addItem("F major"); + ui->keyScaleCombo->addItem("F# major"); + ui->keyScaleCombo->addItem("G major"); + ui->keyScaleCombo->addItem("G# major"); + ui->keyScaleCombo->addItem("A major"); + ui->keyScaleCombo->addItem("A# major"); + ui->keyScaleCombo->addItem("B major"); + ui->keyScaleCombo->addItem("C minor"); + ui->keyScaleCombo->addItem("C# minor"); + ui->keyScaleCombo->addItem("D minor"); + ui->keyScaleCombo->addItem("D# minor"); + ui->keyScaleCombo->addItem("E minor"); + ui->keyScaleCombo->addItem("F minor"); + ui->keyScaleCombo->addItem("F# minor"); + ui->keyScaleCombo->addItem("G minor"); + ui->keyScaleCombo->addItem("G# minor"); + ui->keyScaleCombo->addItem("A minor"); + ui->keyScaleCombo->addItem("A# minor"); + ui->keyScaleCombo->addItem("B minor"); + + if (!song.vocalLanguage.isEmpty()) { - int index = ui->vocalLanguageCombo->findData(vocalLanguage); + int index = ui->vocalLanguageCombo->findData(song.vocalLanguage); if (index >= 0) { ui->vocalLanguageCombo->setCurrentIndex(index); } + else + { + ui->vocalLanguageCombo->addItem(song.vocalLanguage); + ui->vocalLanguageCombo->setCurrentIndex(ui->vocalLanguageCombo->count()-1); + } } else { - ui->vocalLanguageCombo->setCurrentIndex(0); // Default to unset + ui->vocalLanguageCombo->setCurrentIndex(0); } + + if (!song.key.isEmpty()) + { + int index = ui->keyScaleCombo->findText(song.key); + if (index >= 0) + { + ui->keyScaleCombo->setCurrentIndex(index); + } + else + { + ui->keyScaleCombo->addItem(song.key); + ui->keyScaleCombo->setCurrentIndex(ui->keyScaleCombo->count()-1); + } + } + else + { + ui->keyScaleCombo->setCurrentIndex(0); + } + + ui->bpmSpinBox->setValue(song.bpm); } SongDialog::~SongDialog() @@ -90,30 +140,10 @@ SongDialog::~SongDialog() delete ui; } -QString SongDialog::getCaption() const -{ - return ui->captionEdit->toPlainText(); -} - -QString SongDialog::getLyrics() const -{ - return ui->lyricsEdit->toPlainText(); -} - -QString SongDialog::getVocalLanguage() const -{ - return ui->vocalLanguageCombo->currentData().toString(); -} - -bool SongDialog::getCotEnabled() const -{ - return ui->checkBoxEnhanceCaption->isChecked(); -} - void SongDialog::on_okButton_clicked() { // Validate that caption is not empty - QString caption = getCaption(); + QString caption = ui->captionEdit->toPlainText(); if (caption.trimmed().isEmpty()) { QMessageBox::warning(this, "Invalid Input", "Caption cannot be empty."); @@ -126,4 +156,19 @@ void SongDialog::on_okButton_clicked() void SongDialog::on_cancelButton_clicked() { reject(); -} \ No newline at end of file +} + +const SongItem& SongDialog::getSong() +{ + song.caption = ui->captionEdit->toPlainText(); + song.lyrics = ui->lyricsEdit->toPlainText(); + song.vocalLanguage = ui->vocalLanguageCombo->currentData().toString(); + song.cotCaption = ui->checkBoxEnhanceCaption->isChecked(); + if(ui->keyScaleCombo->currentIndex() > 0) + song.key = ui->keyScaleCombo->currentText(); + else + song.key = ""; + song.bpm = ui->bpmSpinBox->value(); + return song; +} + diff --git a/src/SongDialog.h b/src/SongDialog.h index 7230814..1ab08de 100644 --- a/src/SongDialog.h +++ b/src/SongDialog.h @@ -9,6 +9,8 @@ #include #include +#include "SongItem.h" + namespace Ui { class SongDialog; @@ -17,16 +19,13 @@ class SongDialog; class SongDialog : public QDialog { Q_OBJECT + SongItem song; public: - explicit SongDialog(QWidget *parent = nullptr, const QString &caption = "", const QString &lyrics = "", - const QString &vocalLanguage = "", bool cotEnabled = true); + explicit SongDialog(QWidget *parent = nullptr, const SongItem& song = SongItem()); ~SongDialog(); - QString getCaption() const; - QString getLyrics() const; - QString getVocalLanguage() const; - bool getCotEnabled() const; + const SongItem& getSong(); private slots: void on_okButton_clicked(); diff --git a/src/SongDialog.ui b/src/SongDialog.ui index 8c0710a..56e0842 100644 --- a/src/SongDialog.ui +++ b/src/SongDialog.ui @@ -58,21 +58,66 @@ - - - Vocal Language: + + + 5 - - Qt::AlignmentFlag::AlignTop - - + + + + Keyscale + + + + + + + true + + + + + + + Vocal Language: + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter + + + + + + + true + + + + + + + Bpm (0 for llm chooses) + + + + + + + 9999 + + + + - - - true + + + 5 - + + 0 + + diff --git a/src/SongItem.cpp b/src/SongItem.cpp new file mode 100644 index 0000000..68eef23 --- /dev/null +++ b/src/SongItem.cpp @@ -0,0 +1,43 @@ +#include "SongItem.h" + +SongItem::SongItem(const QString &caption, const QString &lyrics) + : caption(caption), lyrics(lyrics), cotCaption(true) +{ + uniqueId = QRandomGenerator::global()->generate64(); +} + +SongItem::SongItem(const QJsonObject& json) +{ + load(json); +} + +void SongItem::store(QJsonObject &json) const +{ + if(!caption.isEmpty()) + json["caption"] = caption; + if(!lyrics.isEmpty()) + json["lyrics"] = lyrics; + json["unique_id"] = QString::number(uniqueId); + json["use_cot_caption"] = cotCaption; + if(!vocalLanguage.isEmpty()) + json["vocal_language"] = vocalLanguage; + if(!key.isEmpty()) + json["keyscale"] = key; + if(bpm != 0) + json["bpm"] = static_cast(bpm); +} + +void SongItem::load(const QJsonObject &json) +{ + caption = json["caption"].toString(); + lyrics = json["lyrics"].toString(); + if(json.contains("unique_id")) + uniqueId = json["unique_id"].toString().toULongLong(); + else + uniqueId = QRandomGenerator::global()->generate64(); + cotCaption = json["use_cot_caption"].toBool(true); + vocalLanguage = json["vocal_language"].toString(); + key = json["keyscale"].toString(); + bpm = json["bpm"].toInt(0); +} + diff --git a/src/SongItem.h b/src/SongItem.h index 7084e0f..a8e993c 100644 --- a/src/SongItem.h +++ b/src/SongItem.h @@ -7,22 +7,25 @@ #include #include #include +#include class SongItem { public: QString caption; QString lyrics; - uint64_t uniqueId; - QString file; + unsigned int bpm; + QString key; QString vocalLanguage; bool cotCaption; + + uint64_t uniqueId; + QString file; QString json; - inline SongItem(const QString &caption = "", const QString &lyrics = "") - : caption(caption), lyrics(lyrics), cotCaption(true) - { - // Generate a unique ID using a cryptographically secure random number - uniqueId = QRandomGenerator::global()->generate64(); - } + SongItem(const QString &caption = "", const QString &lyrics = ""); + SongItem(const QJsonObject& json); + + void store(QJsonObject& json) const; + void load(const QJsonObject& json); }; diff --git a/src/SongListModel.cpp b/src/SongListModel.cpp index a77747a..0b40f05 100644 --- a/src/SongListModel.cpp +++ b/src/SongListModel.cpp @@ -111,6 +111,26 @@ bool SongListModel::setData(const QModelIndex &index, const QVariant &value, int return true; } +void SongListModel::updateSong(const QModelIndex &index, const SongItem& song) +{ + const SongItem &oldSong = songList[index.row()]; + + if(song.caption != oldSong.caption) + { + emit dataChanged(index, index, {CaptionRole}); + } + if(song.lyrics != oldSong.lyrics) + { + emit dataChanged(index, index, {LyricsRole}); + } + if(song.vocalLanguage != oldSong.vocalLanguage) + { + emit dataChanged(index, index, {VocalLanguageRole}); + } + + songList[index.row()] = song; +} + Qt::ItemFlags SongListModel::flags(const QModelIndex &index) const { if (!index.isValid()) diff --git a/src/SongListModel.h b/src/SongListModel.h index c13ff4c..bf1e22e 100644 --- a/src/SongListModel.h +++ b/src/SongListModel.h @@ -37,6 +37,7 @@ public: // Editable: bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + void updateSong(const QModelIndex &index, const SongItem& song); Qt::ItemFlags flags(const QModelIndex& index) const override; // Add/remove songs From 9e0b8b4197b490e36928e8e27e12685b7c06a9c2 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 25 Mar 2026 17:47:48 +0100 Subject: [PATCH 18/40] Fix unitalized bpm for new songs --- src/SongItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SongItem.cpp b/src/SongItem.cpp index 68eef23..4b1686e 100644 --- a/src/SongItem.cpp +++ b/src/SongItem.cpp @@ -1,7 +1,7 @@ #include "SongItem.h" SongItem::SongItem(const QString &caption, const QString &lyrics) - : caption(caption), lyrics(lyrics), cotCaption(true) + : caption(caption), lyrics(lyrics), cotCaption(true), bpm(0) { uniqueId = QRandomGenerator::global()->generate64(); } From 6ef859176966610df9aaa6c91264b777520256a8 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 25 Mar 2026 17:48:21 +0100 Subject: [PATCH 19/40] Double clicking a song ends pause --- src/MainWindow.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index d09e473..9bbda08 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -311,6 +311,7 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index) if (index.column() == 0) { + isPaused = false; if (isPlaying) { audioPlayer->stop(); @@ -318,8 +319,8 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index) else { isPlaying = true; - updateControls(); } + updateControls(); flushGenerationQueue(); ui->nowPlayingLabel->setText("Now Playing: Waiting for generation..."); From 6be80f1d5c0875d1e81c1fb1075e0aaa7fc44ab7 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 25 Mar 2026 18:01:42 +0100 Subject: [PATCH 20/40] Dont allow pause while waiting for song --- src/MainWindow.cpp | 3 ++- src/MainWindow.ui | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 9bbda08..cff9f9f 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -242,7 +242,7 @@ void MainWindow::on_playButton_clicked() void MainWindow::on_pauseButton_clicked() { - if (isPlaying && !isPaused) + if (isPlaying && !isPaused && audioPlayer->isPlaying()) { // Pause playback audioPlayer->pause(); @@ -408,6 +408,7 @@ void MainWindow::playSong(const SongItem& song) ui->nowPlayingLabel->setText("Now Playing: " + song.caption); ui->lyricsTextEdit->setPlainText(song.lyrics); ui->jsonTextEdit->setPlainText(song.json); + updateControls(); } void MainWindow::songGenerated(const SongItem& song) diff --git a/src/MainWindow.ui b/src/MainWindow.ui index 828189b..8d0a9ef 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -178,6 +178,9 @@ + + false + Pause From de7207f07e7d72e0b57a223392a77e8bcaaae723 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 12:24:33 +0200 Subject: [PATCH 21/40] Use the acestop api directly instead of calling binaries --- .gitmodules | 3 + CMakeLists.txt | 33 ++- src/AceStepWorker.cpp | 490 +++++++++++++++++++++++----------- src/AceStepWorker.h | 97 ++++--- src/MainWindow.cpp | 21 +- src/MainWindow.h | 2 +- tests/test_acestep_worker.cpp | 352 ++++++++++++++++++++++++ third_party/acestep.cpp | 1 + 8 files changed, 789 insertions(+), 210 deletions(-) create mode 100644 .gitmodules create mode 100644 tests/test_acestep_worker.cpp create mode 160000 third_party/acestep.cpp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f58ab56 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/acestep.cpp"] + path = third_party/acestep.cpp + url = https://github.com/ServeurpersoCom/acestep.cpp.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 191c922..b1f0110 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Find Qt packages find_package(Qt6 COMPONENTS Core Gui Widgets Multimedia REQUIRED) -# Note: acestep.cpp binaries and models should be provided at runtime +# Add acestep.cpp subdirectory +add_subdirectory(third_party/acestep.cpp) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) @@ -47,21 +48,22 @@ add_executable(${PROJECT_NAME} # UI file target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -# Link libraries (only Qt libraries - acestep.cpp is external) +# Link libraries (Qt + acestep.cpp) target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Multimedia + acestep-core + ggml ) -# Include directories (only our source directory - acestep.cpp is external) +# Include directories target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/third_party/acestep.cpp/src ) -# Note: acestep.cpp binaries (ace-qwen3, dit-vae) and models should be provided at runtime - # Install targets install(TARGETS ${PROJECT_NAME} DESTINATION bin) @@ -71,3 +73,24 @@ install(FILES res/xyz.uvos.aceradio.desktop DESTINATION share/applications) # Install icon files install(FILES res/xyz.uvos.aceradio.png DESTINATION share/icons/hicolor/256x256/apps RENAME xyz.uvos.aceradio.png) install(FILES res/xyz.uvos.aceradio.svg DESTINATION share/icons/hicolor/scalable/apps RENAME xyz.uvos.aceradio.svg) + +# Test executable +add_executable(test_acestep_worker + tests/test_acestep_worker.cpp + src/AceStepWorker.cpp + src/AceStepWorker.h + src/SongItem.cpp + src/SongItem.h +) + +target_link_libraries(test_acestep_worker PRIVATE + Qt6::Core + Qt6::Widgets + acestep-core + ggml +) + +target_include_directories(test_acestep_worker PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/third_party/acestep.cpp/src +) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 50d940c..2e7bb85 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -5,207 +5,379 @@ #include #include #include -#include #include -#include #include -#include #include +#include -AceStep::AceStep(QObject* parent): QObject(parent) +// acestep.cpp headers +#include "pipeline-lm.h" +#include "pipeline-synth.h" +#include "request.h" + +AceStepWorker::AceStepWorker(QObject* parent) + : QObject(parent) { - connect(&qwenProcess, &QProcess::finished, this, &AceStep::qwenProcFinished); - connect(&ditVaeProcess, &QProcess::finished, this, &AceStep::ditProcFinished); } -bool AceStep::isGenerating(SongItem* song) +AceStepWorker::~AceStepWorker() { - if(!busy && song) - *song = this->request.song; - return busy; + cancelGeneration(); + unloadModels(); } -void AceStep::cancelGeneration() +void AceStepWorker::setModelPaths(QString lmPath, QString textEncoderPath, QString ditPath, QString vaePath) { - qwenProcess.blockSignals(true); - qwenProcess.terminate(); - qwenProcess.waitForFinished(); - qwenProcess.blockSignals(false); - - ditVaeProcess.blockSignals(true); - ditVaeProcess.terminate(); - ditVaeProcess.waitForFinished(); - ditVaeProcess.blockSignals(false); - - progressUpdate(100); - if(busy) - generationCanceled(request.song); - - busy = false; + m_lmModelPath = lmPath; + m_textEncoderPath = textEncoderPath; + m_ditPath = ditPath; + m_vaePath = vaePath; + + // Cache as byte arrays to avoid dangling pointers + m_lmModelPathBytes = lmPath.toUtf8(); + m_textEncoderPathBytes = textEncoderPath.toUtf8(); + m_ditPathBytes = ditPath.toUtf8(); + m_vaePathBytes = vaePath.toUtf8(); } -bool AceStep::requestGeneration(SongItem song, QString requestTemplate, QString aceStepPath, - QString qwen3ModelPath, QString textEncoderModelPath, QString ditModelPath, - QString vaeModelPath) +bool AceStepWorker::isGenerating(SongItem* song) { - if(busy) - { - qWarning()<<"Dropping song:"<generate(), aceStepPath, textEncoderModelPath, ditModelPath, vaeModelPath}; +void AceStepWorker::cancelGeneration() +{ + m_cancelRequested.store(true); +} - QString qwen3Binary = aceStepPath + "/ace-lm" + EXE_EXT; - QFileInfo qwen3Info(qwen3Binary); - if (!qwen3Info.exists() || !qwen3Info.isExecutable()) +bool AceStepWorker::requestGeneration(SongItem song, QString requestTemplate) +{ + if (m_busy.load()) { - generationError("ace-lm binary not found at: " + qwen3Binary); - busy = false; - return false; - } - if (!QFileInfo::exists(qwen3ModelPath)) - { - generationError("Qwen3 model not found: " + qwen3ModelPath); - busy = false; - return false; - } - if (!QFileInfo::exists(textEncoderModelPath)) - { - generationError("Text encoder model not found: " + textEncoderModelPath); - busy = false; - return false; - } - if (!QFileInfo::exists(ditModelPath)) - { - generationError("DiT model not found: " + ditModelPath); - busy = false; - return false; - } - if (!QFileInfo::exists(vaeModelPath)) - { - generationError("VAE model not found: " + vaeModelPath); - busy = false; + qWarning() << "Dropping song:" << song.caption; return false; } - request.requestFilePath = tempDir + "/request_" + QString::number(request.uid) + ".json"; + m_busy.store(true); + m_cancelRequested.store(false); + m_currentSong = song; + m_requestTemplate = requestTemplate; + m_uid = QRandomGenerator::global()->generate(); + // Validate model paths + if (m_lmModelPath.isEmpty() || m_textEncoderPath.isEmpty() || + m_ditPath.isEmpty() || m_vaePath.isEmpty()) + { + emit generationError("Model paths not set. Call setModelPaths() first."); + m_busy.store(false); + return false; + } + + // Validate model files exist + if (!QFileInfo::exists(m_lmModelPath)) + { + emit generationError("LM model not found: " + m_lmModelPath); + m_busy.store(false); + return false; + } + if (!QFileInfo::exists(m_textEncoderPath)) + { + emit generationError("Text encoder model not found: " + m_textEncoderPath); + m_busy.store(false); + return false; + } + if (!QFileInfo::exists(m_ditPath)) + { + emit generationError("DiT model not found: " + m_ditPath); + m_busy.store(false); + return false; + } + if (!QFileInfo::exists(m_vaePath)) + { + emit generationError("VAE model not found: " + m_vaePath); + m_busy.store(false); + return false; + } + + // Validate template QJsonParseError parseError; QJsonDocument templateDoc = QJsonDocument::fromJson(requestTemplate.toUtf8(), &parseError); if (!templateDoc.isObject()) { - generationError("Invalid JSON template: " + QString(parseError.errorString())); - busy = false; + emit generationError("Invalid JSON template: " + QString(parseError.errorString())); + m_busy.store(false); return false; } - QJsonObject requestObj = templateDoc.object(); - song.store(requestObj); - - // Write the request file - QFile requestFileHandle(request.requestFilePath); - if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text)) - { - emit generationError("Failed to create request file: " + requestFileHandle.errorString()); - busy = false; - return false; - } - requestFileHandle.write(QJsonDocument(requestObj).toJson(QJsonDocument::Indented)); - requestFileHandle.close(); - - QStringList qwen3Args; - qwen3Args << "--request" << request.requestFilePath; - qwen3Args << "--lm" << qwen3ModelPath; - - progressUpdate(30); - - qwenProcess.start(qwen3Binary, qwen3Args); + // Run generation in the worker thread + QMetaObject::invokeMethod(this, &AceStepWorker::runGeneration, Qt::QueuedConnection); return true; } -void AceStep::qwenProcFinished(int code, QProcess::ExitStatus status) +bool AceStepWorker::checkCancel(void* data) { - QFile::remove(request.requestFilePath); - if(code != 0) - { - QString errorOutput = qwenProcess.readAllStandardError(); - generationError("ace-lm exited with code " + QString::number(code) + ": " + errorOutput); - busy = false; - return; - } - - QString ditVaeBinary = request.aceStepPath + "/ace-synth" + EXE_EXT; - QFileInfo ditVaeInfo(ditVaeBinary); - if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable()) - { - generationError("ace-synth binary not found at: " + ditVaeBinary); - busy = false; - return; - } - - request.requestLlmFilePath = tempDir + "/request_" + QString::number(request.uid) + "0.json"; - if (!QFileInfo::exists(request.requestLlmFilePath)) - { - generationError("ace-lm failed to create enhanced request file "+request.requestLlmFilePath); - busy = false; - return; - } - - // Load lyrics from the enhanced request file - QFile lmOutputFile(request.requestLlmFilePath); - if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text)) - { - QJsonParseError parseError; - request.song.json = lmOutputFile.readAll(); - QJsonDocument doc = QJsonDocument::fromJson(request.song.json.toUtf8(), &parseError); - lmOutputFile.close(); - - if (doc.isObject() && !parseError.error) - { - QJsonObject obj = doc.object(); - if (obj.contains("lyrics") && obj["lyrics"].isString()) - request.song.lyrics = obj["lyrics"].toString(); - } - } - - // Step 2: Run ace-synth to generate audio - QStringList ditVaeArgs; - ditVaeArgs << "--request"<(data); + return worker->m_cancelRequested.load(); } -void AceStep::ditProcFinished(int code, QProcess::ExitStatus status) +void AceStepWorker::runGeneration() { - QFile::remove(request.requestLlmFilePath); - if (code != 0) + // Load models if needed + if (!loadModels()) { - QString errorOutput = ditVaeProcess.readAllStandardError(); - generationError("ace-synth exited with code " + QString::number(code) + ": " + errorOutput); - busy = false; + m_busy.store(false); return; } - // Find the generated WAV file - QString wavFile = tempDir+"/request_" + QString::number(request.uid) + "00.wav"; - if (!QFileInfo::exists(wavFile)) + // Convert SongItem to AceRequest + AceRequest req = songToRequest(m_currentSong, m_requestTemplate); + + // Step 1: LM generates lyrics and audio codes + emit progressUpdate(30); + + AceRequest lmOutput; + request_init(&lmOutput); + + int lmResult = ace_lm_generate(m_lmContext, &req, 1, &lmOutput, + nullptr, nullptr, + checkCancel, this, + LM_MODE_GENERATE); + + if (m_cancelRequested.load()) { - generationError("No WAV file generated at "+wavFile); - busy = false; + emit generationCanceled(m_currentSong); + m_busy.store(false); return; } - busy = false; - progressUpdate(100); - request.song.file = wavFile; - songGenerated(request.song); + if (lmResult != 0) + { + emit generationError("LM generation failed or was canceled"); + m_busy.store(false); + return; + } + + // Update song with generated lyrics + m_currentSong.lyrics = QString::fromStdString(lmOutput.lyrics); + + // Step 2: Synth generates audio + emit progressUpdate(60); + + AceAudio* audioOut = nullptr; + AceAudio outputAudio; + outputAudio.samples = nullptr; + outputAudio.n_samples = 0; + outputAudio.sample_rate = 48000; + + int synthResult = ace_synth_generate(m_synthContext, &lmOutput, + nullptr, 0, // no source audio + nullptr, 0, // no reference audio + 1, &outputAudio, + checkCancel, this); + + if (m_cancelRequested.load()) + { + emit generationCanceled(m_currentSong); + m_busy.store(false); + return; + } + + if (synthResult != 0) + { + emit generationError("Synthesis failed or was canceled"); + m_busy.store(false); + return; + } + + // Save audio to file + QString wavFile = m_tempDir + "/request_" + QString::number(m_uid) + ".wav"; + + // Write WAV file + QFile outFile(wavFile); + if (!outFile.open(QIODevice::WriteOnly)) + { + emit generationError("Failed to create output file: " + outFile.errorString()); + ace_audio_free(&outputAudio); + m_busy.store(false); + return; + } + + // Simple WAV header + stereo float data + int numChannels = 2; + int bitsPerSample = 16; + int byteRate = outputAudio.sample_rate * numChannels * (bitsPerSample / 8); + int blockAlign = numChannels * (bitsPerSample / 8); + int dataSize = outputAudio.n_samples * numChannels * (bitsPerSample / 8); + + // RIFF header + outFile.write("RIFF"); + outFile.write(reinterpret_cast(&dataSize), 4); + outFile.write("WAVE"); + + // fmt chunk + outFile.write("fmt "); + int fmtSize = 16; + outFile.write(reinterpret_cast(&fmtSize), 4); + short audioFormat = 1; // PCM + outFile.write(reinterpret_cast(&audioFormat), 2); + short numCh = numChannels; + outFile.write(reinterpret_cast(&numCh), 2); + int sampleRate = outputAudio.sample_rate; + outFile.write(reinterpret_cast(&sampleRate), 4); + outFile.write(reinterpret_cast(&byteRate), 4); + outFile.write(reinterpret_cast(&blockAlign), 2); + outFile.write(reinterpret_cast(&bitsPerSample), 2); + + // data chunk + outFile.write("data"); + outFile.write(reinterpret_cast(&dataSize), 4); + + // Convert float samples to 16-bit and write + QVector interleaved(outputAudio.n_samples * numChannels); + for (int i = 0; i < outputAudio.n_samples; i++) + { + float left = outputAudio.samples[i]; + float right = outputAudio.samples[i + outputAudio.n_samples]; + // Clamp and convert to 16-bit + left = std::max(-1.0f, std::min(1.0f, left)); + right = std::max(-1.0f, std::min(1.0f, right)); + interleaved[i * 2] = static_cast(left * 32767.0f); + interleaved[i * 2 + 1] = static_cast(right * 32767.0f); + } + outFile.write(reinterpret_cast(interleaved.data()), dataSize); + outFile.close(); + + // Free audio buffer + ace_audio_free(&outputAudio); + + // Store the JSON with all generated fields + m_currentSong.json = QString::fromStdString(request_to_json(&lmOutput, true)); + m_currentSong.file = wavFile; + + // Extract BPM if available + if (lmOutput.bpm > 0) + m_currentSong.bpm = lmOutput.bpm; + + // Extract key if available + if (!lmOutput.keyscale.empty()) + m_currentSong.key = QString::fromStdString(lmOutput.keyscale); + + emit progressUpdate(100); + emit songGenerated(m_currentSong); + + m_busy.store(false); } +bool AceStepWorker::loadModels() +{ + if (m_modelsLoaded.load()) + return true; + + // Load LM + AceLmParams lmParams; + ace_lm_default_params(&lmParams); + lmParams.model_path = m_lmModelPathBytes.constData(); + lmParams.use_fsm = true; + lmParams.use_fa = true; + + m_lmContext = ace_lm_load(&lmParams); + if (!m_lmContext) + { + emit generationError("Failed to load LM model: " + m_lmModelPath); + return false; + } + + // Load Synth + AceSynthParams synthParams; + ace_synth_default_params(&synthParams); + synthParams.text_encoder_path = m_textEncoderPathBytes.constData(); + synthParams.dit_path = m_ditPathBytes.constData(); + synthParams.vae_path = m_vaePathBytes.constData(); + synthParams.use_fa = true; + + m_synthContext = ace_synth_load(&synthParams); + if (!m_synthContext) + { + emit generationError("Failed to load synthesis models"); + ace_lm_free(m_lmContext); + m_lmContext = nullptr; + return false; + } + + m_modelsLoaded.store(true); + return true; +} + +void AceStepWorker::unloadModels() +{ + if (m_synthContext) + { + ace_synth_free(m_synthContext); + m_synthContext = nullptr; + } + if (m_lmContext) + { + ace_lm_free(m_lmContext); + m_lmContext = nullptr; + } + m_modelsLoaded.store(false); +} + +AceRequest AceStepWorker::songToRequest(const SongItem& song, const QString& templateJson) +{ + AceRequest req; + request_init(&req); + + req.caption = song.caption.toStdString(); + req.lyrics = song.lyrics.toStdString(); + req.use_cot_caption = song.cotCaption; + + // Parse template and override defaults + QJsonParseError parseError; + QJsonDocument templateDoc = QJsonDocument::fromJson(templateJson.toUtf8(), &parseError); + if (templateDoc.isObject()) + { + QJsonObject obj = templateDoc.object(); + if (obj.contains("inference_steps")) + req.inference_steps = obj["inference_steps"].toInt(8); + if (obj.contains("shift")) + req.shift = obj["shift"].toDouble(3.0); + if (obj.contains("vocal_language")) + req.vocal_language = obj["vocal_language"].toString().toStdString(); + if (obj.contains("bpm")) + req.bpm = obj["bpm"].toInt(120); + if (obj.contains("duration")) + req.duration = obj["duration"].toDouble(180.0); + if (obj.contains("keyscale")) + req.keyscale = obj["keyscale"].toString().toStdString(); + if (obj.contains("lm_temperature")) + req.lm_temperature = obj["lm_temperature"].toDouble(0.85); + if (obj.contains("lm_cfg_scale")) + req.lm_cfg_scale = obj["lm_cfg_scale"].toDouble(2.0); + } + + // Generate a seed for reproducibility + req.seed = static_cast(QRandomGenerator::global()->generate()); + + return req; +} + +SongItem AceStepWorker::requestToSong(const AceRequest& req, const QString& json) +{ + SongItem song; + song.caption = QString::fromStdString(req.caption); + song.lyrics = QString::fromStdString(req.lyrics); + song.cotCaption = req.use_cot_caption; + + if (req.bpm > 0) + song.bpm = req.bpm; + if (!req.keyscale.empty()) + song.key = QString::fromStdString(req.keyscale); + if (!req.vocal_language.empty()) + song.vocalLanguage = QString::fromStdString(req.vocal_language); + + song.json = json; + return song; +} \ No newline at end of file diff --git a/src/AceStepWorker.h b/src/AceStepWorker.h index 17962f8..e1362aa 100644 --- a/src/AceStepWorker.h +++ b/src/AceStepWorker.h @@ -8,40 +8,34 @@ #include #include -#include +#include #include +#include #include "SongItem.h" -#ifdef Q_OS_WIN -inline const QString EXE_EXT = ".exe"; -#else -inline const QString EXE_EXT = ""; -#endif +// acestep.cpp headers +#include "request.h" -class AceStep : public QObject +struct AceLm; +struct AceSynth; + +class AceStepWorker : public QObject { Q_OBJECT - QProcess qwenProcess; - QProcess ditVaeProcess; - bool busy = false; +public: + explicit AceStepWorker(QObject* parent = nullptr); + ~AceStepWorker(); - struct Request - { - SongItem song; - uint64_t uid; - QString aceStepPath; - QString textEncoderModelPath; - QString ditModelPath; - QString vaeModelPath; - QString requestFilePath; - QString requestLlmFilePath; - }; + bool isGenerating(SongItem* song = nullptr); + void cancelGeneration(); - Request request; + // Model paths - set these before first generation + void setModelPaths(QString lmPath, QString textEncoderPath, QString ditPath, QString vaePath); - const QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + // Request a new song generation + bool requestGeneration(SongItem song, QString requestTemplate); signals: void songGenerated(SongItem song); @@ -49,19 +43,50 @@ signals: void generationError(QString error); void progressUpdate(int progress); -public slots: - bool requestGeneration(SongItem song, QString requestTemplate, QString aceStepPath, - QString qwen3ModelPath, QString textEncoderModelPath, QString ditModelPath, - QString vaeModelPath); - -public: - AceStep(QObject* parent = nullptr); - bool isGenerating(SongItem* song = nullptr); - void cancelGeneration(); - private slots: - void qwenProcFinished(int code, QProcess::ExitStatus status); - void ditProcFinished(int code, QProcess::ExitStatus status); + void runGeneration(); + +private: + // Check if cancellation was requested + static bool checkCancel(void* data); + + // Load models if not already loaded + bool loadModels(); + void unloadModels(); + + // Convert SongItem to AceRequest + AceRequest songToRequest(const SongItem& song, const QString& templateJson); + + // Convert AceRequest back to SongItem + SongItem requestToSong(const AceRequest& req, const QString& json); + + // Generation state + std::atomic m_busy{false}; + std::atomic m_cancelRequested{false}; + std::atomic m_modelsLoaded{false}; + + // Current request data + SongItem m_currentSong; + QString m_requestTemplate; + uint64_t m_uid; + + // Model paths + QString m_lmModelPath; + QString m_textEncoderPath; + QString m_ditPath; + QString m_vaePath; + + // Loaded models (accessed from worker thread only) + AceLm* m_lmContext = nullptr; + AceSynth* m_synthContext = nullptr; + + // Cached model paths as byte arrays (to avoid dangling pointers) + QByteArray m_lmModelPathBytes; + QByteArray m_textEncoderPathBytes; + QByteArray m_ditPathBytes; + QByteArray m_vaePathBytes; + + const QString m_tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); }; -#endif // ACESTEPWORKER_H +#endif // ACESTEPWORKER_H \ No newline at end of file diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index cff9f9f..ba2c79e 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -21,7 +21,7 @@ MainWindow::MainWindow(QWidget *parent) ui(new Ui::MainWindow), songModel(new SongListModel(this)), audioPlayer(new AudioPlayer(this)), - aceStep(new AceStep(this)), + aceStep(new AceStepWorker(this)), playbackTimer(new QTimer(this)), isPlaying(false), isPaused(false), @@ -41,6 +41,9 @@ MainWindow::MainWindow(QWidget *parent) // Load settings loadSettings(); + // Set model paths for acestep.cpp + aceStep->setModelPaths(qwen3ModelPath, textEncoderModelPath, ditModelPath, vaeModelPath); + // Auto-load playlist from config location on startup autoLoadPlaylist(); @@ -62,10 +65,10 @@ MainWindow::MainWindow(QWidget *parent) connect(audioPlayer, &AudioPlayer::playbackStarted, this, &MainWindow::playbackStarted); connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition); connect(audioPlayer, &AudioPlayer::durationChanged, this, &MainWindow::updateDuration); - connect(aceStep, &AceStep::songGenerated, this, &MainWindow::songGenerated); - connect(aceStep, &AceStep::generationCanceled, this, &MainWindow::generationCanceld); - connect(aceStep, &AceStep::generationError, this, &MainWindow::generationError); - connect(aceStep, &AceStep::progressUpdate, ui->progressBar, &QProgressBar::setValue); + connect(aceStep, &AceStepWorker::songGenerated, this, &MainWindow::songGenerated); + connect(aceStep, &AceStepWorker::generationCanceled, this, &MainWindow::generationCanceld); + connect(aceStep, &AceStepWorker::generationError, this, &MainWindow::generationError); + connect(aceStep, &AceStepWorker::progressUpdate, ui->progressBar, &QProgressBar::setValue); // Connect double-click on song list for editing (works with QTableView too) connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); @@ -391,6 +394,9 @@ void MainWindow::on_advancedSettingsButton_clicked() ditModelPath = dialog.getDiTModelPath(); vaeModelPath = dialog.getVAEModelPath(); + // Update model paths for acestep.cpp + aceStep->setModelPaths(qwen3ModelPath, textEncoderModelPath, ditModelPath, vaeModelPath); + saveSettings(); } } @@ -533,10 +539,7 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent) isGeneratingNext = true; ui->statusbar->showMessage("Generateing: "+nextSong.caption); - aceStep->requestGeneration(nextSong, jsonTemplate, - aceStepPath, qwen3ModelPath, - textEncoderModelPath, ditModelPath, - vaeModelPath); + aceStep->requestGeneration(nextSong, jsonTemplate); } void MainWindow::flushGenerationQueue() diff --git a/src/MainWindow.h b/src/MainWindow.h index 9a7406a..9d2a779 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -36,7 +36,7 @@ class MainWindow : public QMainWindow SongListModel *songModel; AudioPlayer *audioPlayer; QThread aceThread; - AceStep *aceStep; + AceStepWorker *aceStep; QTimer *playbackTimer; QString formatTime(int milliseconds); diff --git a/tests/test_acestep_worker.cpp b/tests/test_acestep_worker.cpp new file mode 100644 index 0000000..e360d70 --- /dev/null +++ b/tests/test_acestep_worker.cpp @@ -0,0 +1,352 @@ +// Test for AceStepWorker +// Compile with: cmake .. && make test_acestep_worker && ./test_acestep_worker + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../src/AceStepWorker.h" + +// Test result tracking +static int testsPassed = 0; +static int testsFailed = 0; +static int testsSkipped = 0; + +#define TEST(name) void test_##name() +#define RUN_TEST(name) do { \ + std::cout << "Running " << #name << "... "; \ + test_##name(); \ + if (test_skipped) { \ + std::cout << "SKIPPED" << std::endl; \ + testsSkipped++; \ + test_skipped = false; \ + } else if (test_failed) { \ + std::cout << "FAILED" << std::endl; \ + testsFailed++; \ + test_failed = false; \ + } else { \ + std::cout << "PASSED" << std::endl; \ + testsPassed++; \ + } \ +} while(0) + +static bool test_failed = false; +static bool test_skipped = false; + +#define ASSERT_TRUE(cond) do { \ + if (!(cond)) { \ + std::cout << "FAILED: " << #cond << " at line " << __LINE__ << std::endl; \ + test_failed = true; \ + return; \ + } \ +} while(0) + +#define ASSERT_FALSE(cond) ASSERT_TRUE(!(cond)) + +#define SKIP_IF(cond) do { \ + if (cond) { \ + std::cout << "(skipping: " << #cond << ") "; \ + test_skipped = true; \ + return; \ + } \ +} while(0) + +// Helper to get model paths from settings like main app +struct ModelPaths { + QString lmPath; + QString textEncoderPath; + QString ditPath; + QString vaePath; +}; + +static ModelPaths getModelPathsFromSettings() +{ + ModelPaths paths; + QSettings settings("MusicGenerator", "AceStepGUI"); + + QString appDir = QCoreApplication::applicationDirPath(); + paths.lmPath = settings.value("qwen3ModelPath", + appDir + "/acestep.cpp/models/acestep-5Hz-lm-4B-Q8_0.gguf").toString(); + paths.textEncoderPath = settings.value("textEncoderModelPath", + appDir + "/acestep.cpp/models/Qwen3-Embedding-0.6B-Q8_0.gguf").toString(); + paths.ditPath = settings.value("ditModelPath", + appDir + "/acestep.cpp/models/acestep-v15-turbo-Q8_0.gguf").toString(); + paths.vaePath = settings.value("vaeModelPath", + appDir + "/acestep.cpp/models/vae-BF16.gguf").toString(); + + return paths; +} + +static bool modelsExist(const ModelPaths& paths) +{ + return QFileInfo::exists(paths.lmPath) && + QFileInfo::exists(paths.textEncoderPath) && + QFileInfo::exists(paths.ditPath) && + QFileInfo::exists(paths.vaePath); +} + +// Test 1: Check that isGenerating returns false initially +TEST(initialState) +{ + AceStepWorker worker; + ASSERT_TRUE(!worker.isGenerating()); +} + +// Test 2: Check that requestGeneration returns false when no model paths set +TEST(noModelPaths) +{ + AceStepWorker worker; + SongItem song("test caption", ""); + + bool result = worker.requestGeneration(song, "{}"); + ASSERT_FALSE(result); + ASSERT_TRUE(!worker.isGenerating()); +} + +// Test 3: Check that setModelPaths stores paths correctly +TEST(setModelPaths) +{ + AceStepWorker worker; + worker.setModelPaths("/path/lm.gguf", "/path/encoder.gguf", "/path/dit.gguf", "/path/vae.gguf"); + ASSERT_TRUE(true); +} + +// Test 4: Check async behavior - requestGeneration returns immediately +TEST(asyncReturnsImmediately) +{ + AceStepWorker worker; + worker.setModelPaths("/path/lm.gguf", "/path/encoder.gguf", "/path/dit.gguf", "/path/vae.gguf"); + + SongItem song("test caption", ""); + + // If this blocks, the test will hang + bool result = worker.requestGeneration(song, "{}"); + + // Should return false due to invalid paths, but immediately + ASSERT_FALSE(result); +} + +// Test 5: Check that cancelGeneration sets the cancel flag +TEST(cancellationFlag) +{ + AceStepWorker worker; + worker.setModelPaths("/path/lm.gguf", "/path/encoder.gguf", "/path/dit.gguf", "/path/vae.gguf"); + worker.cancelGeneration(); + ASSERT_TRUE(true); +} + +// Test 6: Check that signals are defined correctly +TEST(signalsExist) +{ + AceStepWorker worker; + + // Verify signals exist by connecting to them (compile-time check) + QObject::connect(&worker, &AceStepWorker::songGenerated, [](const SongItem&) {}); + QObject::connect(&worker, &AceStepWorker::generationCanceled, [](const SongItem&) {}); + QObject::connect(&worker, &AceStepWorker::generationError, [](const QString&) {}); + QObject::connect(&worker, &AceStepWorker::progressUpdate, [](int) {}); + + ASSERT_TRUE(true); +} + +// Test 7: Check SongItem to AceRequest conversion (internal) +TEST(requestConversion) +{ + AceStepWorker worker; + + SongItem song("Upbeat pop rock", "[Verse 1]"); + song.cotCaption = true; + + QString templateJson = R"({"inference_steps": 8, "shift": 3.0, "vocal_language": "en"})"; + + worker.setModelPaths("/path/lm.gguf", "/path/encoder.gguf", "/path/dit.gguf", "/path/vae.gguf"); + bool result = worker.requestGeneration(song, templateJson); + + // Should fail due to invalid paths, but shouldn't crash + ASSERT_FALSE(result); +} + +// Test 8: Read model paths from settings +TEST(readSettings) +{ + ModelPaths paths = getModelPathsFromSettings(); + + std::cout << "\n Model paths from settings:" << std::endl; + std::cout << " LM: " << paths.lmPath.toStdString() << std::endl; + std::cout << " Text Encoder: " << paths.textEncoderPath.toStdString() << std::endl; + std::cout << " DiT: " << paths.ditPath.toStdString() << std::endl; + std::cout << " VAE: " << paths.vaePath.toStdString() << std::endl; + + ASSERT_TRUE(!paths.lmPath.isEmpty()); + ASSERT_TRUE(!paths.textEncoderPath.isEmpty()); + ASSERT_TRUE(!paths.ditPath.isEmpty()); + ASSERT_TRUE(!paths.vaePath.isEmpty()); +} + +// Test 9: Check if model files exist +TEST(checkModelFiles) +{ + ModelPaths paths = getModelPathsFromSettings(); + + bool lmExists = QFileInfo::exists(paths.lmPath); + bool encoderExists = QFileInfo::exists(paths.textEncoderPath); + bool ditExists = QFileInfo::exists(paths.ditPath); + bool vaeExists = QFileInfo::exists(paths.vaePath); + + std::cout << "\n Model file status:" << std::endl; + std::cout << " LM: " << (lmExists ? "EXISTS" : "MISSING") << std::endl; + std::cout << " Text Encoder: " << (encoderExists ? "EXISTS" : "MISSING") << std::endl; + std::cout << " DiT: " << (ditExists ? "EXISTS" : "MISSING") << std::endl; + std::cout << " VAE: " << (vaeExists ? "EXISTS" : "MISSING") << std::endl; + + ASSERT_TRUE(lmExists); + ASSERT_TRUE(encoderExists); + ASSERT_TRUE(ditExists); + ASSERT_TRUE(vaeExists); +} + +// Test 10: Actually generate a song (requires valid model paths) +TEST(generateSong) +{ + ModelPaths paths = getModelPathsFromSettings(); + + // Skip if models don't exist + SKIP_IF(!modelsExist(paths)); + + AceStepWorker worker; + worker.setModelPaths(paths.lmPath, paths.textEncoderPath, paths.ditPath, paths.vaePath); + + SongItem song("Upbeat pop rock with driving guitars", ""); + + QString templateJson = R"({"inference_steps": 8, "shift": 3.0, "vocal_language": "en"})"; + + // Track if we get progress updates + bool gotProgress = false; + QObject::connect(&worker, &AceStepWorker::progressUpdate, [&gotProgress](int p) { + std::cout << "\n Progress: " << p << "%" << std::endl; + gotProgress = true; + }); + + // Track generation result + bool generationCompleted = false; + SongItem resultSong; + QObject::connect(&worker, &AceStepWorker::songGenerated, + [&generationCompleted, &resultSong](const SongItem& song) { + std::cout << "\n Song generated successfully!" << std::endl; + std::cout << " Caption: " << song.caption.toStdString() << std::endl; + std::cout << " Lyrics: " << song.lyrics.left(100).toStdString() << "..." << std::endl; + std::cout << " File: " << song.file.toStdString() << std::endl; + resultSong = song; + generationCompleted = true; + }); + + QString errorMsg; + QObject::connect(&worker, &AceStepWorker::generationError, + [&errorMsg](const QString& err) { + std::cout << "\n Error: " << err.toStdString() << std::endl; + errorMsg = err; + }); + + std::cout << "\n Starting generation..." << std::endl; + + // Request generation + bool result = worker.requestGeneration(song, templateJson); + ASSERT_TRUE(result); + + // Use QEventLoop with timer for proper event processing + QEventLoop loop; + QTimer timeoutTimer; + + timeoutTimer.setSingleShot(true); + timeoutTimer.start(300000); // 5 minute timeout + + QObject::connect(&worker, &AceStepWorker::songGenerated, &loop, &QEventLoop::quit); + QObject::connect(&worker, &AceStepWorker::generationError, &loop, &QEventLoop::quit); + QObject::connect(&timeoutTimer, &QTimer::timeout, &loop, &QEventLoop::quit); + + loop.exec(); + + ASSERT_TRUE(generationCompleted); + ASSERT_TRUE(!resultSong.file.isEmpty()); + ASSERT_TRUE(QFileInfo::exists(resultSong.file)); + + // Check file is not empty + QFileInfo fileInfo(resultSong.file); + std::cout << " File size: " << fileInfo.size() << " bytes" << std::endl; + ASSERT_TRUE(fileInfo.size() > 1000); // Should be at least 1KB for valid audio +} + +// Test 11: Test cancellation +TEST(cancellation) +{ + ModelPaths paths = getModelPathsFromSettings(); + + // Skip if models don't exist + SKIP_IF(!modelsExist(paths)); + + AceStepWorker worker; + worker.setModelPaths(paths.lmPath, paths.textEncoderPath, paths.ditPath, paths.vaePath); + + SongItem song("A very long ambient piece", ""); + + QString templateJson = R"({"inference_steps": 50, "shift": 3.0, "vocal_language": "en"})"; + + bool cancelReceived = false; + QObject::connect(&worker, &AceStepWorker::generationCanceled, + [&cancelReceived](const SongItem&) { + std::cout << "\n Generation was canceled!" << std::endl; + cancelReceived = true; + }); + + std::cout << "\n Starting generation and will cancel after 2 seconds..." << std::endl; + + // Start generation + bool result = worker.requestGeneration(song, templateJson); + ASSERT_TRUE(result); + + // Wait 2 seconds then cancel + QThread::sleep(2); + worker.cancelGeneration(); + + // Wait a bit for cancellation to be processed + QThread::sleep(1); + QCoreApplication::processEvents(); + + // Note: cancellation may or may not complete depending on where in the process + // the cancel was requested. The important thing is it doesn't crash. + std::cout << " Cancel requested, no crash detected" << std::endl; + ASSERT_TRUE(true); +} + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + + std::cout << "=== AceStepWorker Tests ===" << std::endl; + + RUN_TEST(initialState); + RUN_TEST(noModelPaths); + RUN_TEST(setModelPaths); + RUN_TEST(asyncReturnsImmediately); + RUN_TEST(cancellationFlag); + RUN_TEST(signalsExist); + RUN_TEST(requestConversion); + RUN_TEST(readSettings); + RUN_TEST(checkModelFiles); + RUN_TEST(generateSong); + RUN_TEST(cancellation); + + std::cout << "\n=== Results ===" << std::endl; + std::cout << "Passed: " << testsPassed << std::endl; + std::cout << "Skipped: " << testsSkipped << std::endl; + std::cout << "Failed: " << testsFailed << std::endl; + + return testsFailed > 0 ? 1 : 0; +} \ No newline at end of file diff --git a/third_party/acestep.cpp b/third_party/acestep.cpp new file mode 160000 index 0000000..d28398d --- /dev/null +++ b/third_party/acestep.cpp @@ -0,0 +1 @@ +Subproject commit d28398db0ffdb77e8ae071ff31bde8c559e7085a From 14dec9f335ead694f3ca92c07bd063c5ae2ebcc3 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 12:35:44 +0200 Subject: [PATCH 22/40] Keep audio in memory --- src/AceStepWorker.cpp | 45 +++++++++++++---------------------- src/AudioPlayer.cpp | 28 ++++++++++++++++++++++ src/AudioPlayer.h | 2 ++ src/MainWindow.cpp | 26 ++++++++++++++++++-- src/SongItem.h | 2 ++ tests/test_acestep_worker.cpp | 11 ++++----- 6 files changed, 78 insertions(+), 36 deletions(-) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 2e7bb85..c9c9e25 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -191,18 +191,8 @@ void AceStepWorker::runGeneration() return; } - // Save audio to file - QString wavFile = m_tempDir + "/request_" + QString::number(m_uid) + ".wav"; - - // Write WAV file - QFile outFile(wavFile); - if (!outFile.open(QIODevice::WriteOnly)) - { - emit generationError("Failed to create output file: " + outFile.errorString()); - ace_audio_free(&outputAudio); - m_busy.store(false); - return; - } + // Store audio in memory as WAV + auto audioData = std::make_shared(); // Simple WAV header + stereo float data int numChannels = 2; @@ -212,27 +202,27 @@ void AceStepWorker::runGeneration() int dataSize = outputAudio.n_samples * numChannels * (bitsPerSample / 8); // RIFF header - outFile.write("RIFF"); - outFile.write(reinterpret_cast(&dataSize), 4); - outFile.write("WAVE"); + audioData->append("RIFF"); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); + audioData->append("WAVE"); // fmt chunk - outFile.write("fmt "); + audioData->append("fmt "); int fmtSize = 16; - outFile.write(reinterpret_cast(&fmtSize), 4); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&fmtSize), 4)); short audioFormat = 1; // PCM - outFile.write(reinterpret_cast(&audioFormat), 2); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&audioFormat), 2)); short numCh = numChannels; - outFile.write(reinterpret_cast(&numCh), 2); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&numCh), 2)); int sampleRate = outputAudio.sample_rate; - outFile.write(reinterpret_cast(&sampleRate), 4); - outFile.write(reinterpret_cast(&byteRate), 4); - outFile.write(reinterpret_cast(&blockAlign), 2); - outFile.write(reinterpret_cast(&bitsPerSample), 2); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&sampleRate), 4)); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&byteRate), 4)); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&blockAlign), 2)); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&bitsPerSample), 2)); // data chunk - outFile.write("data"); - outFile.write(reinterpret_cast(&dataSize), 4); + audioData->append("data"); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); // Convert float samples to 16-bit and write QVector interleaved(outputAudio.n_samples * numChannels); @@ -246,15 +236,14 @@ void AceStepWorker::runGeneration() interleaved[i * 2] = static_cast(left * 32767.0f); interleaved[i * 2 + 1] = static_cast(right * 32767.0f); } - outFile.write(reinterpret_cast(interleaved.data()), dataSize); - outFile.close(); + audioData->append(QByteArray::fromRawData(reinterpret_cast(interleaved.data()), dataSize)); // Free audio buffer ace_audio_free(&outputAudio); // Store the JSON with all generated fields m_currentSong.json = QString::fromStdString(request_to_json(&lmOutput, true)); - m_currentSong.file = wavFile; + m_currentSong.audioData = audioData; // Extract BPM if available if (lmOutput.bpm > 0) diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp index 2f039bb..bb53670 100644 --- a/src/AudioPlayer.cpp +++ b/src/AudioPlayer.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "AudioPlayer.h" +#include #include AudioPlayer::AudioPlayer(QObject *parent) @@ -48,6 +49,33 @@ void AudioPlayer::play(const QString &filePath) positionTimer->start(); } +void AudioPlayer::play(std::shared_ptr audioData) +{ + if (isPlaying()) + { + stop(); + } + + if (!audioData || audioData->isEmpty()) + { + emit playbackError("No audio data available"); + return; + } + + // Create a buffer with the audio data + QBuffer *buffer = new QBuffer(); + buffer->setData(*audioData); + buffer->open(QIODevice::ReadOnly); + buffer->setParent(this); + + // Use QMediaPlayer::setSourceDevice for in-memory playback + mediaPlayer->setSourceDevice(buffer, QUrl("memory://audio.wav")); + mediaPlayer->play(); + + // Start position timer + positionTimer->start(); +} + void AudioPlayer::play() { if (!isPlaying()) diff --git a/src/AudioPlayer.h b/src/AudioPlayer.h index 6eb4b25..e1d0b47 100644 --- a/src/AudioPlayer.h +++ b/src/AudioPlayer.h @@ -14,6 +14,7 @@ #include #include #include +#include class AudioPlayer : public QObject { @@ -23,6 +24,7 @@ public: ~AudioPlayer(); void play(const QString &filePath); + void play(std::shared_ptr audioData); void play(); void stop(); void pause(); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index ba2c79e..a8b64f7 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -409,7 +409,14 @@ void MainWindow::playbackStarted() void MainWindow::playSong(const SongItem& song) { currentSong = song; - audioPlayer->play(song.file); + if (song.audioData) + { + audioPlayer->play(song.audioData); + } + else if (!song.file.isEmpty()) + { + audioPlayer->play(song.file); + } songModel->setPlayingIndex(songModel->findSongIndexById(song.uniqueId)); ui->nowPlayingLabel->setText("Now Playing: " + song.caption); ui->lyricsTextEdit->setPlainText(song.lyrics); @@ -610,7 +617,22 @@ void MainWindow::on_actionSaveSong() QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return; - QFile::copy(currentSong.file, filePath + ".wav"); + + // Save audio from memory if available, otherwise fall back to file + if (currentSong.audioData) + { + QFile wavFile(filePath + ".wav"); + if (wavFile.open(QIODevice::WriteOnly)) + { + wavFile.write(*currentSong.audioData); + wavFile.close(); + } + } + else if (!currentSong.file.isEmpty()) + { + QFile::copy(currentSong.file, filePath + ".wav"); + } + file.write(jsonData); file.close(); } diff --git a/src/SongItem.h b/src/SongItem.h index a8e993c..0a80926 100644 --- a/src/SongItem.h +++ b/src/SongItem.h @@ -8,6 +8,7 @@ #include #include #include +#include class SongItem { @@ -22,6 +23,7 @@ public: uint64_t uniqueId; QString file; QString json; + std::shared_ptr audioData; SongItem(const QString &caption = "", const QString &lyrics = ""); SongItem(const QJsonObject& json); diff --git a/tests/test_acestep_worker.cpp b/tests/test_acestep_worker.cpp index e360d70..2d32416 100644 --- a/tests/test_acestep_worker.cpp +++ b/tests/test_acestep_worker.cpp @@ -274,13 +274,12 @@ TEST(generateSong) loop.exec(); ASSERT_TRUE(generationCompleted); - ASSERT_TRUE(!resultSong.file.isEmpty()); - ASSERT_TRUE(QFileInfo::exists(resultSong.file)); + ASSERT_TRUE(resultSong.audioData != nullptr); + ASSERT_TRUE(!resultSong.audioData->isEmpty()); - // Check file is not empty - QFileInfo fileInfo(resultSong.file); - std::cout << " File size: " << fileInfo.size() << " bytes" << std::endl; - ASSERT_TRUE(fileInfo.size() > 1000); // Should be at least 1KB for valid audio + // Check audio data is not empty + std::cout << " Audio data size: " << resultSong.audioData->size() << " bytes" << std::endl; + ASSERT_TRUE(resultSong.audioData->size() > 1000); // Should be at least 1KB for valid audio } // Test 11: Test cancellation From 216e59c10595571ab6dc37b1d21219be1d9e1b65 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 13:01:41 +0200 Subject: [PATCH 23/40] Add low vram mode (unloads models) --- src/AceStepWorker.cpp | 474 ++++++++++++++++++++++++--------- src/AceStepWorker.h | 11 + src/AdvancedSettingsDialog.cpp | 10 + src/AdvancedSettingsDialog.h | 2 + src/AdvancedSettingsDialog.ui | 37 +++ src/MainWindow.cpp | 11 + tests/test_acestep_worker.cpp | 174 ++++++++++++ 7 files changed, 597 insertions(+), 122 deletions(-) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index c9c9e25..02ca47f 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -28,6 +28,10 @@ AceStepWorker::~AceStepWorker() void AceStepWorker::setModelPaths(QString lmPath, QString textEncoderPath, QString ditPath, QString vaePath) { + // Check if paths actually changed + bool pathsChanged = (m_lmModelPath != lmPath || m_textEncoderPath != textEncoderPath || + m_ditPath != ditPath || m_vaePath != vaePath); + m_lmModelPath = lmPath; m_textEncoderPath = textEncoderPath; m_ditPath = ditPath; @@ -38,6 +42,17 @@ void AceStepWorker::setModelPaths(QString lmPath, QString textEncoderPath, QStri m_textEncoderPathBytes = textEncoderPath.toUtf8(); m_ditPathBytes = ditPath.toUtf8(); m_vaePathBytes = vaePath.toUtf8(); + + // If paths changed and models are loaded, unload them so they'll be reloaded with new paths + if (pathsChanged && m_modelsLoaded.load()) + { + unloadModels(); + } +} + +void AceStepWorker::setLowVramMode(bool enabled) +{ + m_lowVramMode = enabled; } bool AceStepWorker::isGenerating(SongItem* song) @@ -124,139 +139,295 @@ bool AceStepWorker::checkCancel(void* data) void AceStepWorker::runGeneration() { - // Load models if needed - if (!loadModels()) - { - m_busy.store(false); - return; - } - // Convert SongItem to AceRequest AceRequest req = songToRequest(m_currentSong, m_requestTemplate); - - // Step 1: LM generates lyrics and audio codes - emit progressUpdate(30); - AceRequest lmOutput; request_init(&lmOutput); - int lmResult = ace_lm_generate(m_lmContext, &req, 1, &lmOutput, - nullptr, nullptr, - checkCancel, this, - LM_MODE_GENERATE); - - if (m_cancelRequested.load()) + if (m_lowVramMode) { - emit generationCanceled(m_currentSong); + // Low VRAM mode: load LM → run LM → unload LM → load Synth → run Synth → unload Synth + + // Step 1: Load LM and generate + emit progressUpdate(10); + + if (!loadLm()) + { + m_busy.store(false); + return; + } + + emit progressUpdate(30); + + int lmResult = ace_lm_generate(m_lmContext, &req, 1, &lmOutput, + nullptr, nullptr, + checkCancel, this, + LM_MODE_GENERATE); + + if (m_cancelRequested.load()) + { + unloadLm(); + emit generationCanceled(m_currentSong); + m_busy.store(false); + return; + } + + if (lmResult != 0) + { + unloadLm(); + emit generationError("LM generation failed or was canceled"); + m_busy.store(false); + return; + } + + // Update song with generated lyrics + m_currentSong.lyrics = QString::fromStdString(lmOutput.lyrics); + + // Unload LM to free VRAM + unloadLm(); + + // Step 2: Load Synth and generate audio + emit progressUpdate(50); + + if (!loadSynth()) + { + m_busy.store(false); + return; + } + + emit progressUpdate(60); + + AceAudio outputAudio; + outputAudio.samples = nullptr; + outputAudio.n_samples = 0; + outputAudio.sample_rate = 48000; + + int synthResult = ace_synth_generate(m_synthContext, &lmOutput, + nullptr, 0, // no source audio + nullptr, 0, // no reference audio + 1, &outputAudio, + checkCancel, this); + + // Unload Synth to free VRAM + unloadSynth(); + + if (m_cancelRequested.load()) + { + emit generationCanceled(m_currentSong); + m_busy.store(false); + return; + } + + if (synthResult != 0) + { + emit generationError("Synthesis failed or was canceled"); + m_busy.store(false); + return; + } + + // Store audio in memory as WAV + auto audioData = std::make_shared(); + + // Simple WAV header + stereo float data + int numChannels = 2; + int bitsPerSample = 16; + int byteRate = outputAudio.sample_rate * numChannels * (bitsPerSample / 8); + int blockAlign = numChannels * (bitsPerSample / 8); + int dataSize = outputAudio.n_samples * numChannels * (bitsPerSample / 8); + + // RIFF header + audioData->append("RIFF"); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); + audioData->append("WAVE"); + + // fmt chunk + audioData->append("fmt "); + int fmtSize = 16; + audioData->append(QByteArray::fromRawData(reinterpret_cast(&fmtSize), 4)); + short audioFormat = 1; // PCM + audioData->append(QByteArray::fromRawData(reinterpret_cast(&audioFormat), 2)); + short numCh = numChannels; + audioData->append(QByteArray::fromRawData(reinterpret_cast(&numCh), 2)); + int sampleRate = outputAudio.sample_rate; + audioData->append(QByteArray::fromRawData(reinterpret_cast(&sampleRate), 4)); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&byteRate), 4)); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&blockAlign), 2)); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&bitsPerSample), 2)); + + // data chunk + audioData->append("data"); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); + + // Convert float samples to 16-bit and write + QVector interleaved(outputAudio.n_samples * numChannels); + for (int i = 0; i < outputAudio.n_samples; i++) + { + float left = outputAudio.samples[i]; + float right = outputAudio.samples[i + outputAudio.n_samples]; + // Clamp and convert to 16-bit + left = std::max(-1.0f, std::min(1.0f, left)); + right = std::max(-1.0f, std::min(1.0f, right)); + interleaved[i * 2] = static_cast(left * 32767.0f); + interleaved[i * 2 + 1] = static_cast(right * 32767.0f); + } + audioData->append(QByteArray::fromRawData(reinterpret_cast(interleaved.data()), dataSize)); + + // Free audio buffer + ace_audio_free(&outputAudio); + + // Store the JSON with all generated fields + m_currentSong.json = QString::fromStdString(request_to_json(&lmOutput, true)); + m_currentSong.audioData = audioData; + + // Extract BPM if available + if (lmOutput.bpm > 0) + m_currentSong.bpm = lmOutput.bpm; + + // Extract key if available + if (!lmOutput.keyscale.empty()) + m_currentSong.key = QString::fromStdString(lmOutput.keyscale); + + emit progressUpdate(100); + emit songGenerated(m_currentSong); + m_busy.store(false); - return; } - - if (lmResult != 0) + else { - emit generationError("LM generation failed or was canceled"); + // Normal mode: load all models at start, unload at end + + // Load models if needed + if (!loadModels()) + { + m_busy.store(false); + return; + } + + // Step 1: LM generates lyrics and audio codes + emit progressUpdate(30); + + int lmResult = ace_lm_generate(m_lmContext, &req, 1, &lmOutput, + nullptr, nullptr, + checkCancel, this, + LM_MODE_GENERATE); + + if (m_cancelRequested.load()) + { + emit generationCanceled(m_currentSong); + unloadModels(); + m_busy.store(false); + return; + } + + if (lmResult != 0) + { + emit generationError("LM generation failed or was canceled"); + unloadModels(); + m_busy.store(false); + return; + } + + // Update song with generated lyrics + m_currentSong.lyrics = QString::fromStdString(lmOutput.lyrics); + + // Step 2: Synth generates audio + emit progressUpdate(60); + + AceAudio outputAudio; + outputAudio.samples = nullptr; + outputAudio.n_samples = 0; + outputAudio.sample_rate = 48000; + + int synthResult = ace_synth_generate(m_synthContext, &lmOutput, + nullptr, 0, // no source audio + nullptr, 0, // no reference audio + 1, &outputAudio, + checkCancel, this); + + if (m_cancelRequested.load()) + { + emit generationCanceled(m_currentSong); + unloadModels(); + m_busy.store(false); + return; + } + + if (synthResult != 0) + { + emit generationError("Synthesis failed or was canceled"); + unloadModels(); + m_busy.store(false); + return; + } + + // Store audio in memory as WAV + auto audioData = std::make_shared(); + + // Simple WAV header + stereo float data + int numChannels = 2; + int bitsPerSample = 16; + int byteRate = outputAudio.sample_rate * numChannels * (bitsPerSample / 8); + int blockAlign = numChannels * (bitsPerSample / 8); + int dataSize = outputAudio.n_samples * numChannels * (bitsPerSample / 8); + + // RIFF header + audioData->append("RIFF"); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); + audioData->append("WAVE"); + + // fmt chunk + audioData->append("fmt "); + int fmtSize = 16; + audioData->append(QByteArray::fromRawData(reinterpret_cast(&fmtSize), 4)); + short audioFormat = 1; // PCM + audioData->append(QByteArray::fromRawData(reinterpret_cast(&audioFormat), 2)); + short numCh = numChannels; + audioData->append(QByteArray::fromRawData(reinterpret_cast(&numCh), 2)); + int sampleRate = outputAudio.sample_rate; + audioData->append(QByteArray::fromRawData(reinterpret_cast(&sampleRate), 4)); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&byteRate), 4)); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&blockAlign), 2)); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&bitsPerSample), 2)); + + // data chunk + audioData->append("data"); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); + + // Convert float samples to 16-bit and write + QVector interleaved(outputAudio.n_samples * numChannels); + for (int i = 0; i < outputAudio.n_samples; i++) + { + float left = outputAudio.samples[i]; + float right = outputAudio.samples[i + outputAudio.n_samples]; + // Clamp and convert to 16-bit + left = std::max(-1.0f, std::min(1.0f, left)); + right = std::max(-1.0f, std::min(1.0f, right)); + interleaved[i * 2] = static_cast(left * 32767.0f); + interleaved[i * 2 + 1] = static_cast(right * 32767.0f); + } + audioData->append(QByteArray::fromRawData(reinterpret_cast(interleaved.data()), dataSize)); + + // Free audio buffer + ace_audio_free(&outputAudio); + + // Store the JSON with all generated fields + m_currentSong.json = QString::fromStdString(request_to_json(&lmOutput, true)); + m_currentSong.audioData = audioData; + + // Extract BPM if available + if (lmOutput.bpm > 0) + m_currentSong.bpm = lmOutput.bpm; + + // Extract key if available + if (!lmOutput.keyscale.empty()) + m_currentSong.key = QString::fromStdString(lmOutput.keyscale); + + emit progressUpdate(100); + emit songGenerated(m_currentSong); + + // Keep models loaded for next generation (normal mode) m_busy.store(false); - return; } - - // Update song with generated lyrics - m_currentSong.lyrics = QString::fromStdString(lmOutput.lyrics); - - // Step 2: Synth generates audio - emit progressUpdate(60); - - AceAudio* audioOut = nullptr; - AceAudio outputAudio; - outputAudio.samples = nullptr; - outputAudio.n_samples = 0; - outputAudio.sample_rate = 48000; - - int synthResult = ace_synth_generate(m_synthContext, &lmOutput, - nullptr, 0, // no source audio - nullptr, 0, // no reference audio - 1, &outputAudio, - checkCancel, this); - - if (m_cancelRequested.load()) - { - emit generationCanceled(m_currentSong); - m_busy.store(false); - return; - } - - if (synthResult != 0) - { - emit generationError("Synthesis failed or was canceled"); - m_busy.store(false); - return; - } - - // Store audio in memory as WAV - auto audioData = std::make_shared(); - - // Simple WAV header + stereo float data - int numChannels = 2; - int bitsPerSample = 16; - int byteRate = outputAudio.sample_rate * numChannels * (bitsPerSample / 8); - int blockAlign = numChannels * (bitsPerSample / 8); - int dataSize = outputAudio.n_samples * numChannels * (bitsPerSample / 8); - - // RIFF header - audioData->append("RIFF"); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); - audioData->append("WAVE"); - - // fmt chunk - audioData->append("fmt "); - int fmtSize = 16; - audioData->append(QByteArray::fromRawData(reinterpret_cast(&fmtSize), 4)); - short audioFormat = 1; // PCM - audioData->append(QByteArray::fromRawData(reinterpret_cast(&audioFormat), 2)); - short numCh = numChannels; - audioData->append(QByteArray::fromRawData(reinterpret_cast(&numCh), 2)); - int sampleRate = outputAudio.sample_rate; - audioData->append(QByteArray::fromRawData(reinterpret_cast(&sampleRate), 4)); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&byteRate), 4)); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&blockAlign), 2)); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&bitsPerSample), 2)); - - // data chunk - audioData->append("data"); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); - - // Convert float samples to 16-bit and write - QVector interleaved(outputAudio.n_samples * numChannels); - for (int i = 0; i < outputAudio.n_samples; i++) - { - float left = outputAudio.samples[i]; - float right = outputAudio.samples[i + outputAudio.n_samples]; - // Clamp and convert to 16-bit - left = std::max(-1.0f, std::min(1.0f, left)); - right = std::max(-1.0f, std::min(1.0f, right)); - interleaved[i * 2] = static_cast(left * 32767.0f); - interleaved[i * 2 + 1] = static_cast(right * 32767.0f); - } - audioData->append(QByteArray::fromRawData(reinterpret_cast(interleaved.data()), dataSize)); - - // Free audio buffer - ace_audio_free(&outputAudio); - - // Store the JSON with all generated fields - m_currentSong.json = QString::fromStdString(request_to_json(&lmOutput, true)); - m_currentSong.audioData = audioData; - - // Extract BPM if available - if (lmOutput.bpm > 0) - m_currentSong.bpm = lmOutput.bpm; - - // Extract key if available - if (!lmOutput.keyscale.empty()) - m_currentSong.key = QString::fromStdString(lmOutput.keyscale); - - emit progressUpdate(100); - emit songGenerated(m_currentSong); - - m_busy.store(false); } bool AceStepWorker::loadModels() @@ -314,6 +485,65 @@ void AceStepWorker::unloadModels() m_modelsLoaded.store(false); } +bool AceStepWorker::loadLm() +{ + if (m_lmContext) + return true; + + AceLmParams lmParams; + ace_lm_default_params(&lmParams); + lmParams.model_path = m_lmModelPathBytes.constData(); + lmParams.use_fsm = true; + lmParams.use_fa = true; + + m_lmContext = ace_lm_load(&lmParams); + if (!m_lmContext) + { + emit generationError("Failed to load LM model: " + m_lmModelPath); + return false; + } + return true; +} + +void AceStepWorker::unloadLm() +{ + if (m_lmContext) + { + ace_lm_free(m_lmContext); + m_lmContext = nullptr; + } +} + +bool AceStepWorker::loadSynth() +{ + if (m_synthContext) + return true; + + AceSynthParams synthParams; + ace_synth_default_params(&synthParams); + synthParams.text_encoder_path = m_textEncoderPathBytes.constData(); + synthParams.dit_path = m_ditPathBytes.constData(); + synthParams.vae_path = m_vaePathBytes.constData(); + synthParams.use_fa = true; + + m_synthContext = ace_synth_load(&synthParams); + if (!m_synthContext) + { + emit generationError("Failed to load synthesis models"); + return false; + } + return true; +} + +void AceStepWorker::unloadSynth() +{ + if (m_synthContext) + { + ace_synth_free(m_synthContext); + m_synthContext = nullptr; + } +} + AceRequest AceStepWorker::songToRequest(const SongItem& song, const QString& templateJson) { AceRequest req; diff --git a/src/AceStepWorker.h b/src/AceStepWorker.h index e1362aa..b6498eb 100644 --- a/src/AceStepWorker.h +++ b/src/AceStepWorker.h @@ -34,6 +34,10 @@ public: // Model paths - set these before first generation void setModelPaths(QString lmPath, QString textEncoderPath, QString ditPath, QString vaePath); + // Low VRAM mode: unload models between phases to save VRAM + void setLowVramMode(bool enabled); + bool isLowVramMode() const { return m_lowVramMode; } + // Request a new song generation bool requestGeneration(SongItem song, QString requestTemplate); @@ -54,6 +58,12 @@ private: bool loadModels(); void unloadModels(); + // Individual model load/unload for low VRAM mode + bool loadLm(); + void unloadLm(); + bool loadSynth(); + void unloadSynth(); + // Convert SongItem to AceRequest AceRequest songToRequest(const SongItem& song, const QString& templateJson); @@ -64,6 +74,7 @@ private: std::atomic m_busy{false}; std::atomic m_cancelRequested{false}; std::atomic m_modelsLoaded{false}; + bool m_lowVramMode = false; // Current request data SongItem m_currentSong; diff --git a/src/AdvancedSettingsDialog.cpp b/src/AdvancedSettingsDialog.cpp index a34dc4e..c081a52 100644 --- a/src/AdvancedSettingsDialog.cpp +++ b/src/AdvancedSettingsDialog.cpp @@ -50,6 +50,11 @@ QString AdvancedSettingsDialog::getVAEModelPath() const return ui->vaeModelEdit->text(); } +bool AdvancedSettingsDialog::getLowVramMode() const +{ + return ui->lowVramCheckBox->isChecked(); +} + void AdvancedSettingsDialog::setJsonTemplate(const QString &templateStr) { ui->jsonTemplateEdit->setPlainText(templateStr); @@ -80,6 +85,11 @@ void AdvancedSettingsDialog::setVAEModelPath(const QString &path) ui->vaeModelEdit->setText(path); } +void AdvancedSettingsDialog::setLowVramMode(bool enabled) +{ + ui->lowVramCheckBox->setChecked(enabled); +} + void AdvancedSettingsDialog::on_aceStepBrowseButton_clicked() { QString dir = QFileDialog::getExistingDirectory(this, "Select AceStep Build Directory", ui->aceStepPathEdit->text()); diff --git a/src/AdvancedSettingsDialog.h b/src/AdvancedSettingsDialog.h index 8db247c..3388be8 100644 --- a/src/AdvancedSettingsDialog.h +++ b/src/AdvancedSettingsDialog.h @@ -29,6 +29,7 @@ public: QString getTextEncoderModelPath() const; QString getDiTModelPath() const; QString getVAEModelPath() const; + bool getLowVramMode() const; // Setters for settings void setJsonTemplate(const QString &templateStr); @@ -37,6 +38,7 @@ public: void setTextEncoderModelPath(const QString &path); void setDiTModelPath(const QString &path); void setVAEModelPath(const QString &path); + void setLowVramMode(bool enabled); private slots: void on_aceStepBrowseButton_clicked(); diff --git a/src/AdvancedSettingsDialog.ui b/src/AdvancedSettingsDialog.ui index e9fadf1..987c807 100644 --- a/src/AdvancedSettingsDialog.ui +++ b/src/AdvancedSettingsDialog.ui @@ -19,6 +19,43 @@ 0 + + + Performance + + + + + + Low VRAM Mode + + + + + + + Unload models between generation phases to save VRAM. Slower but uses less memory. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + JSON Template diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index a8b64f7..23ede18 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -155,6 +155,10 @@ void MainWindow::loadSettings() appDir + "/acestep.cpp/models/Qwen3-Embedding-0.6B-BF16.gguf").toString(); ditModelPath = settings.value("ditModelPath", appDir + "/acestep.cpp/models/acestep-v15-turbo-Q8_0.gguf").toString(); vaeModelPath = settings.value("vaeModelPath", appDir + "/acestep.cpp/models/vae-BF16.gguf").toString(); + + // Load low VRAM mode + bool lowVram = settings.value("lowVramMode", false).toBool(); + aceStep->setLowVramMode(lowVram); } void MainWindow::saveSettings() @@ -174,6 +178,9 @@ void MainWindow::saveSettings() settings.setValue("ditModelPath", ditModelPath); settings.setValue("vaeModelPath", vaeModelPath); + // Save low VRAM mode + settings.setValue("lowVramMode", aceStep->isLowVramMode()); + settings.setValue("firstRun", false); } @@ -374,6 +381,7 @@ void MainWindow::on_advancedSettingsButton_clicked() dialog.setTextEncoderModelPath(textEncoderModelPath); dialog.setDiTModelPath(ditModelPath); dialog.setVAEModelPath(vaeModelPath); + dialog.setLowVramMode(aceStep->isLowVramMode()); if (dialog.exec() == QDialog::Accepted) { @@ -397,6 +405,9 @@ void MainWindow::on_advancedSettingsButton_clicked() // Update model paths for acestep.cpp aceStep->setModelPaths(qwen3ModelPath, textEncoderModelPath, ditModelPath, vaeModelPath); + // Update low VRAM mode + aceStep->setLowVramMode(dialog.getLowVramMode()); + saveSettings(); } } diff --git a/tests/test_acestep_worker.cpp b/tests/test_acestep_worker.cpp index 2d32416..60a5ffa 100644 --- a/tests/test_acestep_worker.cpp +++ b/tests/test_acestep_worker.cpp @@ -324,6 +324,177 @@ TEST(cancellation) ASSERT_TRUE(true); } +// Test 12: Test low VRAM mode generation +TEST(generateSongLowVram) +{ + ModelPaths paths = getModelPathsFromSettings(); + + // Skip if models don't exist + SKIP_IF(!modelsExist(paths)); + + AceStepWorker worker; + worker.setModelPaths(paths.lmPath, paths.textEncoderPath, paths.ditPath, paths.vaePath); + worker.setLowVramMode(true); + + ASSERT_TRUE(worker.isLowVramMode()); + + SongItem song("Chill electronic music", ""); + + QString templateJson = R"({"inference_steps": 8, "shift": 3.0, "vocal_language": "en"})"; + + // Track generation result + bool generationCompleted = false; + SongItem resultSong; + QObject::connect(&worker, &AceStepWorker::songGenerated, + [&generationCompleted, &resultSong](const SongItem& song) { + std::cout << "\n Low VRAM mode: Song generated successfully!" << std::endl; + std::cout << " Caption: " << song.caption.toStdString() << std::endl; + if (song.audioData) { + std::cout << " Audio data size: " << song.audioData->size() << " bytes" << std::endl; + } else { + std::cout << " Audio data size: null" << std::endl; + } + resultSong = song; + generationCompleted = true; + }); + + QString errorMsg; + QObject::connect(&worker, &AceStepWorker::generationError, + [&errorMsg](const QString& err) { + std::cout << "\n Error: " << err.toStdString() << std::endl; + errorMsg = err; + }); + + std::cout << "\n Starting low VRAM mode generation..." << std::endl; + + // Request generation + bool result = worker.requestGeneration(song, templateJson); + ASSERT_TRUE(result); + + // Use QEventLoop with timer for proper event processing + QEventLoop loop; + QTimer timeoutTimer; + + timeoutTimer.setSingleShot(true); + timeoutTimer.start(300000); // 5 minute timeout + + QObject::connect(&worker, &AceStepWorker::songGenerated, &loop, &QEventLoop::quit); + QObject::connect(&worker, &AceStepWorker::generationError, &loop, &QEventLoop::quit); + QObject::connect(&timeoutTimer, &QTimer::timeout, &loop, &QEventLoop::quit); + + loop.exec(); + + ASSERT_TRUE(generationCompleted); + ASSERT_TRUE(resultSong.audioData != nullptr); + ASSERT_TRUE(!resultSong.audioData->isEmpty()); + + std::cout << " Audio data size: " << resultSong.audioData->size() << " bytes" << std::endl; + ASSERT_TRUE(resultSong.audioData->size() > 1000); +} + +// Test 13: Test normal mode keeps models loaded between generations +TEST(normalModeKeepsModelsLoaded) +{ + ModelPaths paths = getModelPathsFromSettings(); + + // Skip if models don't exist + SKIP_IF(!modelsExist(paths)); + + AceStepWorker worker; + worker.setModelPaths(paths.lmPath, paths.textEncoderPath, paths.ditPath, paths.vaePath); + // Normal mode is default (lowVramMode = false) + + ASSERT_FALSE(worker.isLowVramMode()); + + QString templateJson = R"({"inference_steps": 8, "shift": 3.0, "vocal_language": "en"})"; + + // Generate first song + bool firstGenerationCompleted = false; + QObject::connect(&worker, &AceStepWorker::songGenerated, + [&firstGenerationCompleted](const SongItem&) { + firstGenerationCompleted = true; + }); + + QObject::connect(&worker, &AceStepWorker::generationError, + [](const QString& err) { + std::cout << "\n Error: " << err.toStdString() << std::endl; + }); + + std::cout << "\n Generating first song (normal mode)..." << std::endl; + + SongItem song1("First song", ""); + bool result = worker.requestGeneration(song1, templateJson); + ASSERT_TRUE(result); + + QEventLoop loop; + QTimer timeoutTimer; + timeoutTimer.setSingleShot(true); + timeoutTimer.start(300000); + + QObject::connect(&worker, &AceStepWorker::songGenerated, &loop, &QEventLoop::quit); + QObject::connect(&worker, &AceStepWorker::generationError, &loop, &QEventLoop::quit); + QObject::connect(&timeoutTimer, &QTimer::timeout, &loop, &QEventLoop::quit); + + loop.exec(); + + ASSERT_TRUE(firstGenerationCompleted); + std::cout << " First generation completed, models should still be loaded" << std::endl; + + // Generate second song - in normal mode this should be faster since models are already loaded + bool secondGenerationCompleted = false; + SongItem secondResult; + QObject::connect(&worker, &AceStepWorker::songGenerated, + [&secondGenerationCompleted, &secondResult](const SongItem& song) { + secondGenerationCompleted = true; + secondResult = song; + }); + + std::cout << " Generating second song (should use cached models)..." << std::endl; + + SongItem song2("Second song", ""); + result = worker.requestGeneration(song2, templateJson); + ASSERT_TRUE(result); + + QEventLoop loop2; + QTimer timeoutTimer2; + timeoutTimer2.setSingleShot(true); + timeoutTimer2.start(300000); + + QObject::connect(&worker, &AceStepWorker::songGenerated, &loop2, &QEventLoop::quit); + QObject::connect(&worker, &AceStepWorker::generationError, &loop2, &QEventLoop::quit); + QObject::connect(&timeoutTimer2, &QTimer::timeout, &loop2, &QEventLoop::quit); + + loop2.exec(); + + ASSERT_TRUE(secondGenerationCompleted); + ASSERT_TRUE(secondResult.audioData != nullptr); + ASSERT_TRUE(!secondResult.audioData->isEmpty()); + + std::cout << " Second generation completed successfully" << std::endl; + std::cout << " Audio data size: " << secondResult.audioData->size() << " bytes" << std::endl; +} + +// Test 14: Test setLowVramMode toggle +TEST(lowVramModeToggle) +{ + AceStepWorker worker; + + // Default should be false (normal mode) + ASSERT_FALSE(worker.isLowVramMode()); + + // Enable low VRAM mode + worker.setLowVramMode(true); + ASSERT_TRUE(worker.isLowVramMode()); + + // Disable low VRAM mode + worker.setLowVramMode(false); + ASSERT_FALSE(worker.isLowVramMode()); + + // Toggle again + worker.setLowVramMode(true); + ASSERT_TRUE(worker.isLowVramMode()); +} + int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); @@ -341,6 +512,9 @@ int main(int argc, char *argv[]) RUN_TEST(checkModelFiles); RUN_TEST(generateSong); RUN_TEST(cancellation); + RUN_TEST(generateSongLowVram); + RUN_TEST(normalModeKeepsModelsLoaded); + RUN_TEST(lowVramModeToggle); std::cout << "\n=== Results ===" << std::endl; std::cout << "Passed: " << testsPassed << std::endl; From 275c1a1852e4298e8be24d8b0eb72982f6778b3b Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 13:17:08 +0200 Subject: [PATCH 24/40] Make fattn optional --- src/AceStepWorker.cpp | 13 +++++++++---- src/AceStepWorker.h | 5 +++++ src/AdvancedSettingsDialog.cpp | 10 ++++++++++ src/AdvancedSettingsDialog.h | 2 ++ src/AdvancedSettingsDialog.ui | 20 ++++++++++++++++++++ src/MainWindow.cpp | 11 +++++++++++ 6 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 02ca47f..17f63f1 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -55,6 +55,11 @@ void AceStepWorker::setLowVramMode(bool enabled) m_lowVramMode = enabled; } +void AceStepWorker::setFlashAttention(bool enabled) +{ + m_flashAttention = enabled; +} + bool AceStepWorker::isGenerating(SongItem* song) { if (!m_busy.load() && song) @@ -440,7 +445,7 @@ bool AceStepWorker::loadModels() ace_lm_default_params(&lmParams); lmParams.model_path = m_lmModelPathBytes.constData(); lmParams.use_fsm = true; - lmParams.use_fa = true; + lmParams.use_fa = m_flashAttention; m_lmContext = ace_lm_load(&lmParams); if (!m_lmContext) @@ -455,7 +460,7 @@ bool AceStepWorker::loadModels() synthParams.text_encoder_path = m_textEncoderPathBytes.constData(); synthParams.dit_path = m_ditPathBytes.constData(); synthParams.vae_path = m_vaePathBytes.constData(); - synthParams.use_fa = true; + synthParams.use_fa = m_flashAttention; m_synthContext = ace_synth_load(&synthParams); if (!m_synthContext) @@ -494,7 +499,7 @@ bool AceStepWorker::loadLm() ace_lm_default_params(&lmParams); lmParams.model_path = m_lmModelPathBytes.constData(); lmParams.use_fsm = true; - lmParams.use_fa = true; + lmParams.use_fa = m_flashAttention; m_lmContext = ace_lm_load(&lmParams); if (!m_lmContext) @@ -524,7 +529,7 @@ bool AceStepWorker::loadSynth() synthParams.text_encoder_path = m_textEncoderPathBytes.constData(); synthParams.dit_path = m_ditPathBytes.constData(); synthParams.vae_path = m_vaePathBytes.constData(); - synthParams.use_fa = true; + synthParams.use_fa = m_flashAttention; m_synthContext = ace_synth_load(&synthParams); if (!m_synthContext) diff --git a/src/AceStepWorker.h b/src/AceStepWorker.h index b6498eb..ba856fa 100644 --- a/src/AceStepWorker.h +++ b/src/AceStepWorker.h @@ -38,6 +38,10 @@ public: void setLowVramMode(bool enabled); bool isLowVramMode() const { return m_lowVramMode; } + // Flash attention mode + void setFlashAttention(bool enabled); + bool isFlashAttention() const { return m_flashAttention; } + // Request a new song generation bool requestGeneration(SongItem song, QString requestTemplate); @@ -75,6 +79,7 @@ private: std::atomic m_cancelRequested{false}; std::atomic m_modelsLoaded{false}; bool m_lowVramMode = false; + bool m_flashAttention = true; // Current request data SongItem m_currentSong; diff --git a/src/AdvancedSettingsDialog.cpp b/src/AdvancedSettingsDialog.cpp index c081a52..fa6b233 100644 --- a/src/AdvancedSettingsDialog.cpp +++ b/src/AdvancedSettingsDialog.cpp @@ -55,6 +55,11 @@ bool AdvancedSettingsDialog::getLowVramMode() const return ui->lowVramCheckBox->isChecked(); } +bool AdvancedSettingsDialog::getFlashAttention() const +{ + return ui->flashAttentionCheckBox->isChecked(); +} + void AdvancedSettingsDialog::setJsonTemplate(const QString &templateStr) { ui->jsonTemplateEdit->setPlainText(templateStr); @@ -90,6 +95,11 @@ void AdvancedSettingsDialog::setLowVramMode(bool enabled) ui->lowVramCheckBox->setChecked(enabled); } +void AdvancedSettingsDialog::setFlashAttention(bool enabled) +{ + ui->flashAttentionCheckBox->setChecked(enabled); +} + void AdvancedSettingsDialog::on_aceStepBrowseButton_clicked() { QString dir = QFileDialog::getExistingDirectory(this, "Select AceStep Build Directory", ui->aceStepPathEdit->text()); diff --git a/src/AdvancedSettingsDialog.h b/src/AdvancedSettingsDialog.h index 3388be8..a648539 100644 --- a/src/AdvancedSettingsDialog.h +++ b/src/AdvancedSettingsDialog.h @@ -30,6 +30,7 @@ public: QString getDiTModelPath() const; QString getVAEModelPath() const; bool getLowVramMode() const; + bool getFlashAttention() const; // Setters for settings void setJsonTemplate(const QString &templateStr); @@ -39,6 +40,7 @@ public: void setDiTModelPath(const QString &path); void setVAEModelPath(const QString &path); void setLowVramMode(bool enabled); + void setFlashAttention(bool enabled); private slots: void on_aceStepBrowseButton_clicked(); diff --git a/src/AdvancedSettingsDialog.ui b/src/AdvancedSettingsDialog.ui index 987c807..3b7f788 100644 --- a/src/AdvancedSettingsDialog.ui +++ b/src/AdvancedSettingsDialog.ui @@ -42,6 +42,26 @@ + + + Flash Attention + + + true + + + + + + + Use flash attention for faster generation. Disable if experiencing issues. + + + true + + + + Qt::Vertical diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 23ede18..b389ac0 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -159,6 +159,10 @@ void MainWindow::loadSettings() // Load low VRAM mode bool lowVram = settings.value("lowVramMode", false).toBool(); aceStep->setLowVramMode(lowVram); + + // Load flash attention setting + bool flashAttention = settings.value("flashAttention", true).toBool(); + aceStep->setFlashAttention(flashAttention); } void MainWindow::saveSettings() @@ -181,6 +185,9 @@ void MainWindow::saveSettings() // Save low VRAM mode settings.setValue("lowVramMode", aceStep->isLowVramMode()); + // Save flash attention setting + settings.setValue("flashAttention", aceStep->isFlashAttention()); + settings.setValue("firstRun", false); } @@ -382,6 +389,7 @@ void MainWindow::on_advancedSettingsButton_clicked() dialog.setDiTModelPath(ditModelPath); dialog.setVAEModelPath(vaeModelPath); dialog.setLowVramMode(aceStep->isLowVramMode()); + dialog.setFlashAttention(aceStep->isFlashAttention()); if (dialog.exec() == QDialog::Accepted) { @@ -408,6 +416,9 @@ void MainWindow::on_advancedSettingsButton_clicked() // Update low VRAM mode aceStep->setLowVramMode(dialog.getLowVramMode()); + // Update flash attention setting + aceStep->setFlashAttention(dialog.getFlashAttention()); + saveSettings(); } } From 39f6429199889c11c1cacefa8618b16af4df756a Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 10:58:29 +0200 Subject: [PATCH 25/40] Fix qt connection warning --- src/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index b389ac0..0b932a4 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -74,7 +74,7 @@ MainWindow::MainWindow(QWidget *parent) connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); // Connect audio player error signal - connect(audioPlayer, &AudioPlayer::playbackError, [this](const QString &error) + connect(audioPlayer, &AudioPlayer::playbackError, this, [this](const QString &error) { QMessageBox::warning(this, "Playback Error", "Failed to play audio: " + error); }); From ef17335614e9ebab580f54108c5a2612f97a750b Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 11:06:28 +0200 Subject: [PATCH 26/40] Start qthread --- src/MainWindow.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 0b932a4..d862b49 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -21,7 +21,7 @@ MainWindow::MainWindow(QWidget *parent) ui(new Ui::MainWindow), songModel(new SongListModel(this)), audioPlayer(new AudioPlayer(this)), - aceStep(new AceStepWorker(this)), + aceStep(new AceStepWorker), playbackTimer(new QTimer(this)), isPlaying(false), isPaused(false), @@ -101,6 +101,8 @@ MainWindow::MainWindow(QWidget *parent) ui->nowPlayingLabel->setText("Now Playing:"); currentSong = songModel->getSong(0); + + aceThread.start(); } MainWindow::~MainWindow() @@ -568,7 +570,7 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent) isGeneratingNext = true; ui->statusbar->showMessage("Generateing: "+nextSong.caption); - aceStep->requestGeneration(nextSong, jsonTemplate); + QMetaObject::invokeMethod(aceStep, &AceStepWorker::requestGeneration, Qt::QueuedConnection, nextSong, jsonTemplate); } void MainWindow::flushGenerationQueue() From be21c1f2bd29f78c58f306043648839b3b9c4b42 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 11:16:52 +0200 Subject: [PATCH 27/40] Dont unload models on cancle in high vram mode --- src/AceStepWorker.cpp | 3 +-- src/MainWindow.cpp | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 17f63f1..b401de8 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -319,14 +319,13 @@ void AceStepWorker::runGeneration() if (m_cancelRequested.load()) { emit generationCanceled(m_currentSong); - unloadModels(); m_busy.store(false); return; } if (lmResult != 0) { - emit generationError("LM generation failed or was canceled"); + emit generationError("LM generation failed"); unloadModels(); m_busy.store(false); return; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index d862b49..16b07e1 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -102,6 +102,8 @@ MainWindow::MainWindow(QWidget *parent) currentSong = songModel->getSong(0); + // Start the worker thread and enter its event loop + QObject::connect(&aceThread, &QThread::started, [this]() {qDebug() << "Worker thread started";}); aceThread.start(); } From 55be24b36fb3c19b3d07648e40a37bf92d527022 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 11:52:38 +0200 Subject: [PATCH 28/40] Refactor generation pipeline to reduce code duplication --- src/AceStepWorker.cpp | 425 +++++++++++++----------------------------- src/AceStepWorker.h | 8 +- 2 files changed, 130 insertions(+), 303 deletions(-) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index b401de8..731d021 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -8,11 +8,9 @@ #include #include #include -#include // acestep.cpp headers #include "pipeline-lm.h" -#include "pipeline-synth.h" #include "request.h" AceStepWorker::AceStepWorker(QObject* parent) @@ -142,6 +140,56 @@ bool AceStepWorker::checkCancel(void* data) return worker->m_cancelRequested.load(); } +std::shared_ptr AceStepWorker::convertToWav(const AceAudio& audio) +{ + auto audioData = std::make_shared(); + + // Simple WAV header + stereo float data + int numChannels = 2; + int bitsPerSample = 16; + int byteRate = audio.sample_rate * numChannels * (bitsPerSample / 8); + int blockAlign = numChannels * (bitsPerSample / 8); + int dataSize = audio.n_samples * numChannels * (bitsPerSample / 8); + + // RIFF header + audioData->append("RIFF"); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); + audioData->append("WAVE"); + + // fmt chunk + audioData->append("fmt "); + int fmtSize = 16; + audioData->append(QByteArray::fromRawData(reinterpret_cast(&fmtSize), 4)); + short audioFormat = 1; // PCM + audioData->append(QByteArray::fromRawData(reinterpret_cast(&audioFormat), 2)); + short numCh = numChannels; + audioData->append(QByteArray::fromRawData(reinterpret_cast(&numCh), 2)); + int sampleRate = audio.sample_rate; + audioData->append(QByteArray::fromRawData(reinterpret_cast(&sampleRate), 4)); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&byteRate), 4)); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&blockAlign), 2)); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&bitsPerSample), 2)); + + // data chunk + audioData->append("data"); + audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); + + // Convert float samples to 16-bit and write + QVector interleaved(audio.n_samples * numChannels); + for (int i = 0; i < audio.n_samples; i++) + { + float left = audio.samples[i]; + float right = audio.samples[i + audio.n_samples]; + // Clamp and convert to 16-bit + left = std::max(-1.0f, std::min(1.0f, left)); + right = std::max(-1.0f, std::min(1.0f, right)); + interleaved[i * 2] = static_cast(left * 32767.0f); + interleaved[i * 2 + 1] = static_cast(right * 32767.0f); + } + audioData->append(QByteArray::fromRawData(reinterpret_cast(interleaved.data()), dataSize)); + return audioData; +} + void AceStepWorker::runGeneration() { // Convert SongItem to AceRequest @@ -149,328 +197,109 @@ void AceStepWorker::runGeneration() AceRequest lmOutput; request_init(&lmOutput); - if (m_lowVramMode) + emit progressUpdate(10); + + if (!loadLm()) { - // Low VRAM mode: load LM → run LM → unload LM → load Synth → run Synth → unload Synth + m_busy.store(false); + return; + } - // Step 1: Load LM and generate - emit progressUpdate(10); + emit progressUpdate(30); - if (!loadLm()) - { - m_busy.store(false); - return; - } + int lmResult = ace_lm_generate(m_lmContext, &req, 1, &lmOutput, + nullptr, nullptr, + checkCancel, this, + LM_MODE_GENERATE); - emit progressUpdate(30); + if (m_cancelRequested.load()) + { + if(m_lowVramMode) + unloadModels(); + emit generationCanceled(m_currentSong); + m_busy.store(false); + return; + } - int lmResult = ace_lm_generate(m_lmContext, &req, 1, &lmOutput, - nullptr, nullptr, - checkCancel, this, - LM_MODE_GENERATE); + if (lmResult != 0) + { + if(m_lowVramMode) + unloadModels(); + emit generationError("LM generation failed or was canceled"); + m_busy.store(false); + return; + } - if (m_cancelRequested.load()) - { - unloadLm(); - emit generationCanceled(m_currentSong); - m_busy.store(false); - return; - } + m_currentSong.lyrics = QString::fromStdString(lmOutput.lyrics); - if (lmResult != 0) - { - unloadLm(); - emit generationError("LM generation failed or was canceled"); - m_busy.store(false); - return; - } - - // Update song with generated lyrics - m_currentSong.lyrics = QString::fromStdString(lmOutput.lyrics); - - // Unload LM to free VRAM + if(m_lowVramMode) unloadLm(); - // Step 2: Load Synth and generate audio - emit progressUpdate(50); + emit progressUpdate(50); - if (!loadSynth()) - { - m_busy.store(false); - return; - } + if (!loadSynth()) + { + m_busy.store(false); + return; + } - emit progressUpdate(60); + emit progressUpdate(60); - AceAudio outputAudio; - outputAudio.samples = nullptr; - outputAudio.n_samples = 0; - outputAudio.sample_rate = 48000; + AceAudio outputAudio; + outputAudio.samples = nullptr; + outputAudio.n_samples = 0; + outputAudio.sample_rate = 48000; - int synthResult = ace_synth_generate(m_synthContext, &lmOutput, - nullptr, 0, // no source audio - nullptr, 0, // no reference audio - 1, &outputAudio, - checkCancel, this); + int synthResult = ace_synth_generate(m_synthContext, &lmOutput, + nullptr, 0, // no source audio + nullptr, 0, // no reference audio + 1, &outputAudio, + checkCancel, this); - // Unload Synth to free VRAM + if(m_lowVramMode) unloadSynth(); - if (m_cancelRequested.load()) - { - emit generationCanceled(m_currentSong); - m_busy.store(false); - return; - } - - if (synthResult != 0) - { - emit generationError("Synthesis failed or was canceled"); - m_busy.store(false); - return; - } - - // Store audio in memory as WAV - auto audioData = std::make_shared(); - - // Simple WAV header + stereo float data - int numChannels = 2; - int bitsPerSample = 16; - int byteRate = outputAudio.sample_rate * numChannels * (bitsPerSample / 8); - int blockAlign = numChannels * (bitsPerSample / 8); - int dataSize = outputAudio.n_samples * numChannels * (bitsPerSample / 8); - - // RIFF header - audioData->append("RIFF"); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); - audioData->append("WAVE"); - - // fmt chunk - audioData->append("fmt "); - int fmtSize = 16; - audioData->append(QByteArray::fromRawData(reinterpret_cast(&fmtSize), 4)); - short audioFormat = 1; // PCM - audioData->append(QByteArray::fromRawData(reinterpret_cast(&audioFormat), 2)); - short numCh = numChannels; - audioData->append(QByteArray::fromRawData(reinterpret_cast(&numCh), 2)); - int sampleRate = outputAudio.sample_rate; - audioData->append(QByteArray::fromRawData(reinterpret_cast(&sampleRate), 4)); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&byteRate), 4)); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&blockAlign), 2)); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&bitsPerSample), 2)); - - // data chunk - audioData->append("data"); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); - - // Convert float samples to 16-bit and write - QVector interleaved(outputAudio.n_samples * numChannels); - for (int i = 0; i < outputAudio.n_samples; i++) - { - float left = outputAudio.samples[i]; - float right = outputAudio.samples[i + outputAudio.n_samples]; - // Clamp and convert to 16-bit - left = std::max(-1.0f, std::min(1.0f, left)); - right = std::max(-1.0f, std::min(1.0f, right)); - interleaved[i * 2] = static_cast(left * 32767.0f); - interleaved[i * 2 + 1] = static_cast(right * 32767.0f); - } - audioData->append(QByteArray::fromRawData(reinterpret_cast(interleaved.data()), dataSize)); - - // Free audio buffer - ace_audio_free(&outputAudio); - - // Store the JSON with all generated fields - m_currentSong.json = QString::fromStdString(request_to_json(&lmOutput, true)); - m_currentSong.audioData = audioData; - - // Extract BPM if available - if (lmOutput.bpm > 0) - m_currentSong.bpm = lmOutput.bpm; - - // Extract key if available - if (!lmOutput.keyscale.empty()) - m_currentSong.key = QString::fromStdString(lmOutput.keyscale); - - emit progressUpdate(100); - emit songGenerated(m_currentSong); - - m_busy.store(false); - } - else + if (m_cancelRequested.load()) { - // Normal mode: load all models at start, unload at end - - // Load models if needed - if (!loadModels()) - { - m_busy.store(false); - return; - } - - // Step 1: LM generates lyrics and audio codes - emit progressUpdate(30); - - int lmResult = ace_lm_generate(m_lmContext, &req, 1, &lmOutput, - nullptr, nullptr, - checkCancel, this, - LM_MODE_GENERATE); - - if (m_cancelRequested.load()) - { - emit generationCanceled(m_currentSong); - m_busy.store(false); - return; - } - - if (lmResult != 0) - { - emit generationError("LM generation failed"); - unloadModels(); - m_busy.store(false); - return; - } - - // Update song with generated lyrics - m_currentSong.lyrics = QString::fromStdString(lmOutput.lyrics); - - // Step 2: Synth generates audio - emit progressUpdate(60); - - AceAudio outputAudio; - outputAudio.samples = nullptr; - outputAudio.n_samples = 0; - outputAudio.sample_rate = 48000; - - int synthResult = ace_synth_generate(m_synthContext, &lmOutput, - nullptr, 0, // no source audio - nullptr, 0, // no reference audio - 1, &outputAudio, - checkCancel, this); - - if (m_cancelRequested.load()) - { - emit generationCanceled(m_currentSong); - unloadModels(); - m_busy.store(false); - return; - } - - if (synthResult != 0) - { - emit generationError("Synthesis failed or was canceled"); - unloadModels(); - m_busy.store(false); - return; - } - - // Store audio in memory as WAV - auto audioData = std::make_shared(); - - // Simple WAV header + stereo float data - int numChannels = 2; - int bitsPerSample = 16; - int byteRate = outputAudio.sample_rate * numChannels * (bitsPerSample / 8); - int blockAlign = numChannels * (bitsPerSample / 8); - int dataSize = outputAudio.n_samples * numChannels * (bitsPerSample / 8); - - // RIFF header - audioData->append("RIFF"); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); - audioData->append("WAVE"); - - // fmt chunk - audioData->append("fmt "); - int fmtSize = 16; - audioData->append(QByteArray::fromRawData(reinterpret_cast(&fmtSize), 4)); - short audioFormat = 1; // PCM - audioData->append(QByteArray::fromRawData(reinterpret_cast(&audioFormat), 2)); - short numCh = numChannels; - audioData->append(QByteArray::fromRawData(reinterpret_cast(&numCh), 2)); - int sampleRate = outputAudio.sample_rate; - audioData->append(QByteArray::fromRawData(reinterpret_cast(&sampleRate), 4)); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&byteRate), 4)); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&blockAlign), 2)); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&bitsPerSample), 2)); - - // data chunk - audioData->append("data"); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); - - // Convert float samples to 16-bit and write - QVector interleaved(outputAudio.n_samples * numChannels); - for (int i = 0; i < outputAudio.n_samples; i++) - { - float left = outputAudio.samples[i]; - float right = outputAudio.samples[i + outputAudio.n_samples]; - // Clamp and convert to 16-bit - left = std::max(-1.0f, std::min(1.0f, left)); - right = std::max(-1.0f, std::min(1.0f, right)); - interleaved[i * 2] = static_cast(left * 32767.0f); - interleaved[i * 2 + 1] = static_cast(right * 32767.0f); - } - audioData->append(QByteArray::fromRawData(reinterpret_cast(interleaved.data()), dataSize)); - - // Free audio buffer - ace_audio_free(&outputAudio); - - // Store the JSON with all generated fields - m_currentSong.json = QString::fromStdString(request_to_json(&lmOutput, true)); - m_currentSong.audioData = audioData; - - // Extract BPM if available - if (lmOutput.bpm > 0) - m_currentSong.bpm = lmOutput.bpm; - - // Extract key if available - if (!lmOutput.keyscale.empty()) - m_currentSong.key = QString::fromStdString(lmOutput.keyscale); - - emit progressUpdate(100); - emit songGenerated(m_currentSong); - - // Keep models loaded for next generation (normal mode) + emit generationCanceled(m_currentSong); m_busy.store(false); + return; } + + if (synthResult != 0) + { + emit generationError("Synthesis failed or was canceled"); + m_busy.store(false); + return; + } + + std::shared_ptr audioData = convertToWav(outputAudio); + ace_audio_free(&outputAudio); + + m_currentSong.json = QString::fromStdString(request_to_json(&lmOutput, true)); + m_currentSong.audioData = audioData; + + if (lmOutput.bpm > 0) + m_currentSong.bpm = lmOutput.bpm; + + if (!lmOutput.keyscale.empty()) + m_currentSong.key = QString::fromStdString(lmOutput.keyscale); + + emit progressUpdate(100); + emit songGenerated(m_currentSong); + + m_busy.store(false); } bool AceStepWorker::loadModels() { - if (m_modelsLoaded.load()) - return true; - - // Load LM - AceLmParams lmParams; - ace_lm_default_params(&lmParams); - lmParams.model_path = m_lmModelPathBytes.constData(); - lmParams.use_fsm = true; - lmParams.use_fa = m_flashAttention; - - m_lmContext = ace_lm_load(&lmParams); - if (!m_lmContext) - { - emit generationError("Failed to load LM model: " + m_lmModelPath); + bool ret = loadSynth(); + if(!ret) return false; - } - // Load Synth - AceSynthParams synthParams; - ace_synth_default_params(&synthParams); - synthParams.text_encoder_path = m_textEncoderPathBytes.constData(); - synthParams.dit_path = m_ditPathBytes.constData(); - synthParams.vae_path = m_vaePathBytes.constData(); - synthParams.use_fa = m_flashAttention; - - m_synthContext = ace_synth_load(&synthParams); - if (!m_synthContext) - { - emit generationError("Failed to load synthesis models"); - ace_lm_free(m_lmContext); - m_lmContext = nullptr; + ret = loadLm(); + if(!ret) return false; - } - - m_modelsLoaded.store(true); return true; } diff --git a/src/AceStepWorker.h b/src/AceStepWorker.h index ba856fa..ec8c8a6 100644 --- a/src/AceStepWorker.h +++ b/src/AceStepWorker.h @@ -13,8 +13,7 @@ #include #include "SongItem.h" - -// acestep.cpp headers +#include "pipeline-synth.h" #include "request.h" struct AceLm; @@ -74,6 +73,8 @@ private: // Convert AceRequest back to SongItem SongItem requestToSong(const AceRequest& req, const QString& json); + static std::shared_ptr convertToWav(const AceAudio& audio); + // Generation state std::atomic m_busy{false}; std::atomic m_cancelRequested{false}; @@ -96,13 +97,10 @@ private: AceLm* m_lmContext = nullptr; AceSynth* m_synthContext = nullptr; - // Cached model paths as byte arrays (to avoid dangling pointers) QByteArray m_lmModelPathBytes; QByteArray m_textEncoderPathBytes; QByteArray m_ditPathBytes; QByteArray m_vaePathBytes; - - const QString m_tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); }; #endif // ACESTEPWORKER_H \ No newline at end of file From e3fb4761b00f9c21aa2fe417fcb93f8985dc9493 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 11:52:58 +0200 Subject: [PATCH 29/40] Make fattn off the default due to vulkan issues --- src/AdvancedSettingsDialog.ui | 518 +++++++++++++++++----------------- src/MainWindow.cpp | 5 +- 2 files changed, 263 insertions(+), 260 deletions(-) diff --git a/src/AdvancedSettingsDialog.ui b/src/AdvancedSettingsDialog.ui index 3b7f788..9c9cd4a 100644 --- a/src/AdvancedSettingsDialog.ui +++ b/src/AdvancedSettingsDialog.ui @@ -1,260 +1,260 @@ - - AdvancedSettingsDialog - - - - 0 - 0 - 600 - 450 - - - - Advanced Settings - - - - - - 0 - - - - Performance - - - - - - Low VRAM Mode - - - - - - - Unload models between generation phases to save VRAM. Slower but uses less memory. - - - true - - - - - - - Flash Attention - - - true - - - - - - - Use flash attention for faster generation. Disable if experiencing issues. - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - JSON Template - - - - - - JSON Template for AceStep generation: - - - true - - - - - - - - - - - Model Paths - - - - QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow - - - - - AceStep Path: - - - - - - - - - - - - Browse... - - - - - - - - - Qwen3 Model: - - - - - - - - - - - - Browse... - - - - - - - - - Text Encoder Model: - - - - - - - - - - - - Browse... - - - - - - - - - DiT Model: - - - - - - - - - - - - Browse... - - - - - - - - - VAE Model: - - - - - - - - - - - - Browse... - - - - - - - - - - - - - QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save - - - - - - - - - buttonBox - accepted() - AdvancedSettingsDialog - accept() - - - 248 - 254 - - - 157 - 254 - - - - - buttonBox - rejected() - AdvancedSettingsDialog - reject() - - - 316 - 260 - - - 286 - 260 - - - - - + + AdvancedSettingsDialog + + + + 0 + 0 + 600 + 450 + + + + Advanced Settings + + + + + + 0 + + + + Performance + + + + + + Low VRAM Mode + + + + + + + Unload models between generation phases to save VRAM. Slower but uses less memory. + + + true + + + + + + + Flash Attention + + + true + + + + + + + Use flash attention for faster generation. Disable if experiencing poor output quality on vulkan. + + + true + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + JSON Template + + + + + + JSON Template for AceStep generation: + + + true + + + + + + + + + + + Model Paths + + + + QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow + + + + + AceStep Path: + + + + + + + + + + + + Browse... + + + + + + + + + Qwen3 Model: + + + + + + + + + + + + Browse... + + + + + + + + + Text Encoder Model: + + + + + + + + + + + + Browse... + + + + + + + + + DiT Model: + + + + + + + + + + + + Browse... + + + + + + + + + VAE Model: + + + + + + + + + + + + Browse... + + + + + + + + + + + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save + + + + + + + + + buttonBox + accepted() + AdvancedSettingsDialog + accept() + + + 248 + 254 + + + 157 + 254 + + + + + buttonBox + rejected() + AdvancedSettingsDialog + reject() + + + 316 + 260 + + + 286 + 260 + + + + + diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 16b07e1..e8b0551 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -29,6 +29,7 @@ MainWindow::MainWindow(QWidget *parent) isGeneratingNext(false) { aceStep->moveToThread(&aceThread); + aceThread.setObjectName("AceStep Woker Thread"); ui->setupUi(this); @@ -110,6 +111,8 @@ MainWindow::MainWindow(QWidget *parent) MainWindow::~MainWindow() { aceStep->cancelGeneration(); + aceThread.quit(); + aceThread.wait(); autoSavePlaylist(); saveSettings(); @@ -165,7 +168,7 @@ void MainWindow::loadSettings() aceStep->setLowVramMode(lowVram); // Load flash attention setting - bool flashAttention = settings.value("flashAttention", true).toBool(); + bool flashAttention = settings.value("flashAttention", false).toBool(); aceStep->setFlashAttention(flashAttention); } From cc72f360b9b91877e4f783cdbe59b1b271da85c8 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 12:11:38 +0200 Subject: [PATCH 30/40] Improve AceStep request creation --- src/AceStepWorker.cpp | 72 ++++++++++--------------------------------- src/AceStepWorker.h | 8 ----- 2 files changed, 17 insertions(+), 63 deletions(-) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 731d021..8804613 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -119,16 +119,6 @@ bool AceStepWorker::requestGeneration(SongItem song, QString requestTemplate) return false; } - // Validate template - QJsonParseError parseError; - QJsonDocument templateDoc = QJsonDocument::fromJson(requestTemplate.toUtf8(), &parseError); - if (!templateDoc.isObject()) - { - emit generationError("Invalid JSON template: " + QString(parseError.errorString())); - m_busy.store(false); - return false; - } - // Run generation in the worker thread QMetaObject::invokeMethod(this, &AceStepWorker::runGeneration, Qt::QueuedConnection); return true; @@ -382,54 +372,26 @@ AceRequest AceStepWorker::songToRequest(const SongItem& song, const QString& tem AceRequest req; request_init(&req); - req.caption = song.caption.toStdString(); - req.lyrics = song.lyrics.toStdString(); - req.use_cot_caption = song.cotCaption; - - // Parse template and override defaults - QJsonParseError parseError; - QJsonDocument templateDoc = QJsonDocument::fromJson(templateJson.toUtf8(), &parseError); - if (templateDoc.isObject()) + // Parse template first to get all default values + QJsonObject requestJson; + if (!templateJson.isEmpty()) { - QJsonObject obj = templateDoc.object(); - if (obj.contains("inference_steps")) - req.inference_steps = obj["inference_steps"].toInt(8); - if (obj.contains("shift")) - req.shift = obj["shift"].toDouble(3.0); - if (obj.contains("vocal_language")) - req.vocal_language = obj["vocal_language"].toString().toStdString(); - if (obj.contains("bpm")) - req.bpm = obj["bpm"].toInt(120); - if (obj.contains("duration")) - req.duration = obj["duration"].toDouble(180.0); - if (obj.contains("keyscale")) - req.keyscale = obj["keyscale"].toString().toStdString(); - if (obj.contains("lm_temperature")) - req.lm_temperature = obj["lm_temperature"].toDouble(0.85); - if (obj.contains("lm_cfg_scale")) - req.lm_cfg_scale = obj["lm_cfg_scale"].toDouble(2.0); + QJsonParseError parseError; + QJsonDocument templateDoc = QJsonDocument::fromJson(templateJson.toUtf8(), &parseError); + if (templateDoc.isObject()) + requestJson = templateDoc.object(); + else + qWarning() << "Failed to parse request template:" << parseError.errorString(); } - // Generate a seed for reproducibility - req.seed = static_cast(QRandomGenerator::global()->generate()); + song.store(requestJson); + QJsonDocument requestJsonDoc(requestJson); + QString requestJsonString = QString::fromUtf8(requestJsonDoc.toJson(QJsonDocument::Compact)); + if (!request_parse_json(&req, requestJsonString.toUtf8())) + qWarning() << "Failed to parse merged request JSON"; + + if (req.seed < 0) + req.seed = static_cast(QRandomGenerator::global()->generate()); return req; -} - -SongItem AceStepWorker::requestToSong(const AceRequest& req, const QString& json) -{ - SongItem song; - song.caption = QString::fromStdString(req.caption); - song.lyrics = QString::fromStdString(req.lyrics); - song.cotCaption = req.use_cot_caption; - - if (req.bpm > 0) - song.bpm = req.bpm; - if (!req.keyscale.empty()) - song.key = QString::fromStdString(req.keyscale); - if (!req.vocal_language.empty()) - song.vocalLanguage = QString::fromStdString(req.vocal_language); - - song.json = json; - return song; } \ No newline at end of file diff --git a/src/AceStepWorker.h b/src/AceStepWorker.h index ec8c8a6..56d95b1 100644 --- a/src/AceStepWorker.h +++ b/src/AceStepWorker.h @@ -54,25 +54,17 @@ private slots: void runGeneration(); private: - // Check if cancellation was requested static bool checkCancel(void* data); - // Load models if not already loaded bool loadModels(); void unloadModels(); - - // Individual model load/unload for low VRAM mode bool loadLm(); void unloadLm(); bool loadSynth(); void unloadSynth(); - // Convert SongItem to AceRequest AceRequest songToRequest(const SongItem& song, const QString& templateJson); - // Convert AceRequest back to SongItem - SongItem requestToSong(const AceRequest& req, const QString& json); - static std::shared_ptr convertToWav(const AceAudio& audio); // Generation state From f85bbb326e098123ebcd6dcf63ac0505ff6105a5 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 12:15:07 +0200 Subject: [PATCH 31/40] Remove acestep binary path machinery --- src/AdvancedSettingsDialog.cpp | 19 ------------------ src/AdvancedSettingsDialog.h | 3 --- src/AdvancedSettingsDialog.ui | 35 +++++++--------------------------- src/MainWindow.cpp | 4 ---- src/MainWindow.h | 1 - 5 files changed, 7 insertions(+), 55 deletions(-) diff --git a/src/AdvancedSettingsDialog.cpp b/src/AdvancedSettingsDialog.cpp index fa6b233..96ef3c4 100644 --- a/src/AdvancedSettingsDialog.cpp +++ b/src/AdvancedSettingsDialog.cpp @@ -25,11 +25,6 @@ QString AdvancedSettingsDialog::getJsonTemplate() const return ui->jsonTemplateEdit->toPlainText(); } -QString AdvancedSettingsDialog::getAceStepPath() const -{ - return ui->aceStepPathEdit->text(); -} - QString AdvancedSettingsDialog::getQwen3ModelPath() const { return ui->qwen3ModelEdit->text(); @@ -65,11 +60,6 @@ void AdvancedSettingsDialog::setJsonTemplate(const QString &templateStr) ui->jsonTemplateEdit->setPlainText(templateStr); } -void AdvancedSettingsDialog::setAceStepPath(const QString &path) -{ - ui->aceStepPathEdit->setText(path); -} - void AdvancedSettingsDialog::setQwen3ModelPath(const QString &path) { ui->qwen3ModelEdit->setText(path); @@ -100,15 +90,6 @@ void AdvancedSettingsDialog::setFlashAttention(bool enabled) ui->flashAttentionCheckBox->setChecked(enabled); } -void AdvancedSettingsDialog::on_aceStepBrowseButton_clicked() -{ - QString dir = QFileDialog::getExistingDirectory(this, "Select AceStep Build Directory", ui->aceStepPathEdit->text()); - if (!dir.isEmpty()) - { - ui->aceStepPathEdit->setText(dir); - } -} - void AdvancedSettingsDialog::on_qwen3BrowseButton_clicked() { QString file = QFileDialog::getOpenFileName(this, "Select Qwen3 Model", ui->qwen3ModelEdit->text(), diff --git a/src/AdvancedSettingsDialog.h b/src/AdvancedSettingsDialog.h index a648539..2e00f3f 100644 --- a/src/AdvancedSettingsDialog.h +++ b/src/AdvancedSettingsDialog.h @@ -24,7 +24,6 @@ public: // Getters for settings QString getJsonTemplate() const; - QString getAceStepPath() const; QString getQwen3ModelPath() const; QString getTextEncoderModelPath() const; QString getDiTModelPath() const; @@ -34,7 +33,6 @@ public: // Setters for settings void setJsonTemplate(const QString &templateStr); - void setAceStepPath(const QString &path); void setQwen3ModelPath(const QString &path); void setTextEncoderModelPath(const QString &path); void setDiTModelPath(const QString &path); @@ -43,7 +41,6 @@ public: void setFlashAttention(bool enabled); private slots: - void on_aceStepBrowseButton_clicked(); void on_qwen3BrowseButton_clicked(); void on_textEncoderBrowseButton_clicked(); void on_ditBrowseButton_clicked(); diff --git a/src/AdvancedSettingsDialog.ui b/src/AdvancedSettingsDialog.ui index 9c9cd4a..6e95e90 100644 --- a/src/AdvancedSettingsDialog.ui +++ b/src/AdvancedSettingsDialog.ui @@ -105,34 +105,13 @@ QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow - - - AceStep Path: - - - - - - - - - - - - Browse... - - - - - - Qwen3 Model: - + @@ -146,14 +125,14 @@ - + Text Encoder Model: - + @@ -167,14 +146,14 @@ - + DiT Model: - + @@ -188,14 +167,14 @@ - + VAE Model: - + diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index e8b0551..3f8c90e 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -155,7 +155,6 @@ void MainWindow::loadSettings() // Load path settings with defaults based on application directory QString appDir = QCoreApplication::applicationDirPath(); - aceStepPath = settings.value("aceStepPath", appDir + "/acestep.cpp").toString(); qwen3ModelPath = settings.value("qwen3ModelPath", appDir + "/acestep.cpp/models/acestep-5Hz-lm-4B-Q8_0.gguf").toString(); textEncoderModelPath = settings.value("textEncoderModelPath", @@ -183,7 +182,6 @@ void MainWindow::saveSettings() settings.setValue("shuffleMode", shuffleMode); // Save path settings - settings.setValue("aceStepPath", aceStepPath); settings.setValue("qwen3ModelPath", qwen3ModelPath); settings.setValue("textEncoderModelPath", textEncoderModelPath); settings.setValue("ditModelPath", ditModelPath); @@ -390,7 +388,6 @@ void MainWindow::on_advancedSettingsButton_clicked() // Set current values dialog.setJsonTemplate(jsonTemplate); - dialog.setAceStepPath(aceStepPath); dialog.setQwen3ModelPath(qwen3ModelPath); dialog.setTextEncoderModelPath(textEncoderModelPath); dialog.setDiTModelPath(ditModelPath); @@ -411,7 +408,6 @@ void MainWindow::on_advancedSettingsButton_clicked() // Update settings jsonTemplate = dialog.getJsonTemplate(); - aceStepPath = dialog.getAceStepPath(); qwen3ModelPath = dialog.getQwen3ModelPath(); textEncoderModelPath = dialog.getTextEncoderModelPath(); ditModelPath = dialog.getDiTModelPath(); diff --git a/src/MainWindow.h b/src/MainWindow.h index 9d2a779..d87142c 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -50,7 +50,6 @@ class MainWindow : public QMainWindow QString jsonTemplate; // Path settings - QString aceStepPath; QString qwen3ModelPath; QString textEncoderModelPath; QString ditModelPath; From c7f5c97e7bc47e61274db6268ae92fdf744521ee Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 13:00:35 +0200 Subject: [PATCH 32/40] Discontinue the use of qt connect by name slots --- src/AdvancedSettingsDialog.cpp | 14 +++++++--- src/AdvancedSettingsDialog.h | 8 +++--- src/MainWindow.cpp | 51 +++++++++++++++++++--------------- src/MainWindow.h | 28 +++++++++---------- src/SongDialog.cpp | 8 ++++-- src/SongDialog.h | 4 +-- 6 files changed, 65 insertions(+), 48 deletions(-) diff --git a/src/AdvancedSettingsDialog.cpp b/src/AdvancedSettingsDialog.cpp index 96ef3c4..7e2e412 100644 --- a/src/AdvancedSettingsDialog.cpp +++ b/src/AdvancedSettingsDialog.cpp @@ -13,6 +13,12 @@ AdvancedSettingsDialog::AdvancedSettingsDialog(QWidget *parent) ui(new Ui::AdvancedSettingsDialog) { ui->setupUi(this); + + // Connect signals and slots explicitly + connect(ui->qwen3BrowseButton, &QPushButton::clicked, this, &AdvancedSettingsDialog::onQwen3BrowseButtonClicked); + connect(ui->textEncoderBrowseButton, &QPushButton::clicked, this, &AdvancedSettingsDialog::onTextEncoderBrowseButtonClicked); + connect(ui->ditBrowseButton, &QPushButton::clicked, this, &AdvancedSettingsDialog::onDiTBrowseButtonClicked); + connect(ui->vaeBrowseButton, &QPushButton::clicked, this, &AdvancedSettingsDialog::onVAEBrowseButtonClicked); } AdvancedSettingsDialog::~AdvancedSettingsDialog() @@ -90,7 +96,7 @@ void AdvancedSettingsDialog::setFlashAttention(bool enabled) ui->flashAttentionCheckBox->setChecked(enabled); } -void AdvancedSettingsDialog::on_qwen3BrowseButton_clicked() +void AdvancedSettingsDialog::onQwen3BrowseButtonClicked() { QString file = QFileDialog::getOpenFileName(this, "Select Qwen3 Model", ui->qwen3ModelEdit->text(), "GGUF Files (*.gguf)"); @@ -100,7 +106,7 @@ void AdvancedSettingsDialog::on_qwen3BrowseButton_clicked() } } -void AdvancedSettingsDialog::on_textEncoderBrowseButton_clicked() +void AdvancedSettingsDialog::onTextEncoderBrowseButtonClicked() { QString file = QFileDialog::getOpenFileName(this, "Select Text Encoder Model", ui->textEncoderEdit->text(), "GGUF Files (*.gguf)"); @@ -110,7 +116,7 @@ void AdvancedSettingsDialog::on_textEncoderBrowseButton_clicked() } } -void AdvancedSettingsDialog::on_ditBrowseButton_clicked() +void AdvancedSettingsDialog::onDiTBrowseButtonClicked() { QString file = QFileDialog::getOpenFileName(this, "Select DiT Model", ui->ditModelEdit->text(), "GGUF Files (*.gguf)"); if (!file.isEmpty()) @@ -119,7 +125,7 @@ void AdvancedSettingsDialog::on_ditBrowseButton_clicked() } } -void AdvancedSettingsDialog::on_vaeBrowseButton_clicked() +void AdvancedSettingsDialog::onVAEBrowseButtonClicked() { QString file = QFileDialog::getOpenFileName(this, "Select VAE Model", ui->vaeModelEdit->text(), "GGUF Files (*.gguf)"); if (!file.isEmpty()) diff --git a/src/AdvancedSettingsDialog.h b/src/AdvancedSettingsDialog.h index 2e00f3f..8a3047a 100644 --- a/src/AdvancedSettingsDialog.h +++ b/src/AdvancedSettingsDialog.h @@ -41,10 +41,10 @@ public: void setFlashAttention(bool enabled); private slots: - void on_qwen3BrowseButton_clicked(); - void on_textEncoderBrowseButton_clicked(); - void on_ditBrowseButton_clicked(); - void on_vaeBrowseButton_clicked(); + void onQwen3BrowseButtonClicked(); + void onTextEncoderBrowseButtonClicked(); + void onDiTBrowseButtonClicked(); + void onVAEBrowseButtonClicked(); private: Ui::AdvancedSettingsDialog *ui; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 3f8c90e..5ed9b5a 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -49,11 +49,18 @@ MainWindow::MainWindow(QWidget *parent) autoLoadPlaylist(); // Connect signals and slots - connect(ui->actionAdvancedSettings, &QAction::triggered, this, &MainWindow::on_advancedSettingsButton_clicked); - connect(ui->actionSavePlaylist, &QAction::triggered, this, &MainWindow::on_actionSavePlaylist); - connect(ui->actionLoadPlaylist, &QAction::triggered, this, &MainWindow::on_actionLoadPlaylist); - connect(ui->actionAppendPlaylist, &QAction::triggered, this, &MainWindow::on_actionAppendPlaylist); - connect(ui->actionSaveSong, &QAction::triggered, this, &MainWindow::on_actionSaveSong); + connect(ui->playButton, &QPushButton::clicked, this, &MainWindow::onPlayButtonClicked); + connect(ui->pauseButton, &QPushButton::clicked, this, &MainWindow::onPauseButtonClicked); + connect(ui->skipButton, &QPushButton::clicked, this, &MainWindow::onSkipButtonClicked); + connect(ui->stopButton, &QPushButton::clicked, this, &MainWindow::onStopButtonClicked); + connect(ui->shuffleButton, &QPushButton::clicked, this, &MainWindow::onShuffleButtonClicked); + connect(ui->addSongButton, &QPushButton::clicked, this, &MainWindow::onAddSongButtonClicked); + connect(ui->removeSongButton, &QPushButton::clicked, this, &MainWindow::onRemoveSongButtonClicked); + connect(ui->actionAdvancedSettings, &QAction::triggered, this, &MainWindow::onAdvancedSettingsButtonClicked); + connect(ui->actionSavePlaylist, &QAction::triggered, this, &MainWindow::onActionSavePlaylist); + connect(ui->actionLoadPlaylist, &QAction::triggered, this, &MainWindow::onActionLoadPlaylist); + connect(ui->actionAppendPlaylist, &QAction::triggered, this, &MainWindow::onActionAppendPlaylist); + connect(ui->actionSaveSong, &QAction::triggered, this, &MainWindow::onActionSaveSong); connect(ui->actionQuit, &QAction::triggered, this, [this]() { close(); @@ -72,7 +79,7 @@ MainWindow::MainWindow(QWidget *parent) connect(aceStep, &AceStepWorker::progressUpdate, ui->progressBar, &QProgressBar::setValue); // Connect double-click on song list for editing (works with QTableView too) - connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); + connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::onSongListViewDoubleClicked); // Connect audio player error signal connect(audioPlayer, &AudioPlayer::playbackError, this, [this](const QString &error) @@ -241,7 +248,7 @@ void MainWindow::updateControls() ui->removeSongButton->setEnabled(hasSongs && ui->songListView->currentIndex().isValid()); } -void MainWindow::on_playButton_clicked() +void MainWindow::onPlayButtonClicked() { if (isPaused) { @@ -262,7 +269,7 @@ void MainWindow::on_playButton_clicked() updateControls(); } -void MainWindow::on_pauseButton_clicked() +void MainWindow::onPauseButtonClicked() { if (isPlaying && !isPaused && audioPlayer->isPlaying()) { @@ -273,7 +280,7 @@ void MainWindow::on_pauseButton_clicked() } } -void MainWindow::on_skipButton_clicked() +void MainWindow::onSkipButtonClicked() { if (isPlaying) { @@ -283,7 +290,7 @@ void MainWindow::on_skipButton_clicked() } } -void MainWindow::on_stopButton_clicked() +void MainWindow::onStopButtonClicked() { if (isPlaying) { @@ -297,7 +304,7 @@ void MainWindow::on_stopButton_clicked() } } -void MainWindow::on_shuffleButton_clicked() +void MainWindow::onShuffleButtonClicked() { shuffleMode = ui->shuffleButton->isChecked(); updateControls(); @@ -307,7 +314,7 @@ void MainWindow::on_shuffleButton_clicked() ensureSongsInQueue(); } -void MainWindow::on_addSongButton_clicked() +void MainWindow::onAddSongButtonClicked() { SongDialog dialog(this); @@ -322,12 +329,12 @@ void MainWindow::on_addSongButton_clicked() } } -void MainWindow::on_songListView_doubleClicked(const QModelIndex &index) +void MainWindow::onSongListViewDoubleClicked(const QModelIndex &index) { if (!index.isValid()) return; - disconnect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); + disconnect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::onSongListViewDoubleClicked); int row = index.row(); @@ -359,10 +366,10 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index) songModel->updateSong(songModel->index(row, 1), dialog.getSong()); } - connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); + connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::onSongListViewDoubleClicked); } -void MainWindow::on_removeSongButton_clicked() +void MainWindow::onRemoveSongButtonClicked() { QModelIndex currentIndex = ui->songListView->currentIndex(); if (!currentIndex.isValid()) @@ -382,7 +389,7 @@ void MainWindow::on_removeSongButton_clicked() } } -void MainWindow::on_advancedSettingsButton_clicked() +void MainWindow::onAdvancedSettingsButtonClicked() { AdvancedSettingsDialog dialog(this); @@ -532,7 +539,7 @@ void MainWindow::updatePlaybackStatus(bool playing) updateControls(); } -void MainWindow::on_positionSlider_sliderMoved(int position) +void MainWindow::onPositionSliderSliderMoved(int position) { if (isPlaying && audioPlayer->isPlaying()) { @@ -582,7 +589,7 @@ void MainWindow::flushGenerationQueue() } // Playlist save/load methods -void MainWindow::on_actionSavePlaylist() +void MainWindow::onActionSavePlaylist() { QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/playlist.json", @@ -594,7 +601,7 @@ void MainWindow::on_actionSavePlaylist() } } -void MainWindow::on_actionLoadPlaylist() +void MainWindow::onActionLoadPlaylist() { QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), @@ -607,7 +614,7 @@ void MainWindow::on_actionLoadPlaylist() } } -void MainWindow::on_actionAppendPlaylist() +void MainWindow::onActionAppendPlaylist() { QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), @@ -618,7 +625,7 @@ void MainWindow::on_actionAppendPlaylist() } } -void MainWindow::on_actionSaveSong() +void MainWindow::onActionSaveSong() { QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/song.json", diff --git a/src/MainWindow.h b/src/MainWindow.h index d87142c..6b3663f 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -67,19 +67,19 @@ public slots: void show(); private slots: - void on_playButton_clicked(); - void on_pauseButton_clicked(); - void on_skipButton_clicked(); - void on_stopButton_clicked(); - void on_shuffleButton_clicked(); - void on_positionSlider_sliderMoved(int position); + void onPlayButtonClicked(); + void onPauseButtonClicked(); + void onSkipButtonClicked(); + void onStopButtonClicked(); + void onShuffleButtonClicked(); + void onPositionSliderSliderMoved(int position); void updatePosition(int position); void updateDuration(int duration); - void on_addSongButton_clicked(); - void on_removeSongButton_clicked(); - void on_advancedSettingsButton_clicked(); + void onAddSongButtonClicked(); + void onRemoveSongButtonClicked(); + void onAdvancedSettingsButtonClicked(); - void on_songListView_doubleClicked(const QModelIndex &index); + void onSongListViewDoubleClicked(const QModelIndex &index); void songGenerated(const SongItem& song); void generationCanceld(const SongItem& song); @@ -88,10 +88,10 @@ private slots: void updatePlaybackStatus(bool playing); void generationError(const QString &error); - void on_actionSavePlaylist(); - void on_actionLoadPlaylist(); - void on_actionAppendPlaylist(); - void on_actionSaveSong(); + void onActionSavePlaylist(); + void onActionLoadPlaylist(); + void onActionAppendPlaylist(); + void onActionSaveSong(); private: void loadSettings(); diff --git a/src/SongDialog.cpp b/src/SongDialog.cpp index 58f4ec6..e0ebde8 100644 --- a/src/SongDialog.cpp +++ b/src/SongDialog.cpp @@ -12,6 +12,10 @@ SongDialog::SongDialog(QWidget *parent, const SongItem &song) { ui->setupUi(this); + // Connect signals and slots explicitly + connect(ui->okButton, &QPushButton::clicked, this, &SongDialog::onOkButtonClicked); + connect(ui->cancelButton, &QPushButton::clicked, this, &SongDialog::onCancelButtonClicked); + ui->captionEdit->setPlainText(song.caption); ui->lyricsEdit->setPlainText(song.lyrics); ui->checkBoxEnhanceCaption->setChecked(song.cotCaption); @@ -140,7 +144,7 @@ SongDialog::~SongDialog() delete ui; } -void SongDialog::on_okButton_clicked() +void SongDialog::onOkButtonClicked() { // Validate that caption is not empty QString caption = ui->captionEdit->toPlainText(); @@ -153,7 +157,7 @@ void SongDialog::on_okButton_clicked() accept(); } -void SongDialog::on_cancelButton_clicked() +void SongDialog::onCancelButtonClicked() { reject(); } diff --git a/src/SongDialog.h b/src/SongDialog.h index 1ab08de..5e4ce48 100644 --- a/src/SongDialog.h +++ b/src/SongDialog.h @@ -28,8 +28,8 @@ public: const SongItem& getSong(); private slots: - void on_okButton_clicked(); - void on_cancelButton_clicked(); + void onOkButtonClicked(); + void onCancelButtonClicked(); private: Ui::SongDialog *ui; From ac29d2629b14284b503d0af9035cb7a2cf16b893 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 13:04:29 +0200 Subject: [PATCH 33/40] Update the readme to the new reality of integrated acestep.cpp --- README.md | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b5e9d35..d35041b 100644 --- a/README.md +++ b/README.md @@ -12,19 +12,11 @@ A C++ Qt graphical user interface for generating music using [acestep.cpp](https ## Installing -1. Grab the latest release from https://github.com/IMbackK/aceradio/releases/ -2. For now you will have to provide acestep.cpp binaries, these can be optained by following the instructions at https://github.com/ServeurpersoCom/acestep.cpp or the **Build acestep.cpp** section below +Grab the latest release from https://github.com/IMbackK/aceradio/releases/ -## Build acestep.cpp: +## Models -```bash -git clone https://github.com/ServeurpersoCom/acestep.cpp.git -cd acestep.cpp -mkdir build && cd build -cmake .. -DGGML_VULKAN=ON # or other backend -make -j$(nproc) -./models.sh # Download models (requires ~7.7 GB free space) -``` +grab the models from [here](https://huggingface.co/Serveurperso/ACE-Step-1.5-GGUF/tree/main) ## Building @@ -62,7 +54,7 @@ Run windeployqt to copy necessary Qt libraries to the build folder: ## Setup Paths: -Go to settings->Ace Step->Model Paths and add the paths to the acestep.cpp binaries the models. +Go to settings->Ace Step->Model Paths and add the paths to the AceStep models. ## License From 35d87197dccbf524594cb81721a233f14f63e249 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 13:08:51 +0200 Subject: [PATCH 34/40] Improve build instructions --- README.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d35041b..1b7bfe9 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,14 @@ ![Screenshot](res/scrot.png) A C++ Qt graphical user interface for generating music using [acestep.cpp](https://github.com/ServeurpersoCom/acestep.cpp). +Provides endless playlist style music generation on ROCm, Vulkan or CUDA. ## Requirements - Qt 6 Core, Gui, Widgets, and Multimedia - CMake 3.14+ - acestep.cpp (bring your own binaries) +- ROCm, cuda or vulkan headers and shader compiler ## Installing @@ -21,13 +23,17 @@ grab the models from [here](https://huggingface.co/Serveurperso/ACE-Step-1.5-GGU ## Building ### Linux / macOS -```bash -git clone https://github.com/IMbackK/aceradio.git -cd aceradio -mkdir build && cd build -cmake .. -make -j$(nproc) -``` + +1. clone the repo and create a build directory + * `git clone https://github.com/IMbackK/aceradio.git && cd aceradio` +2. create a build directory and enter it + * `mkdir build && cd build` +3. configure the project + * `cmake -DGGML_HIP=on ..` for ROCm support. + * `cmake -DGGML_CUDA=on ..` for CUDA support. + * `cmake -DGGML_VULKAN=on ..` for Vulkan support. +4. build the project + `make -j$(nproc)` ### Windows (Qt6 + CMake) From b3185e4bc592d40b17d6339f3281ede1acb4bad6 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 13:17:47 +0200 Subject: [PATCH 35/40] CI: Build vulkan version --- .github/actions/linux-setup-vulkan/action.yml | 20 ++++++++++++++ .github/actions/unarchive-tar/action.yml | 27 +++++++++++++++++++ .github/workflows/build.yml | 19 +++++++++++-- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 .github/actions/linux-setup-vulkan/action.yml create mode 100644 .github/actions/unarchive-tar/action.yml diff --git a/.github/actions/linux-setup-vulkan/action.yml b/.github/actions/linux-setup-vulkan/action.yml new file mode 100644 index 0000000..4d29837 --- /dev/null +++ b/.github/actions/linux-setup-vulkan/action.yml @@ -0,0 +1,20 @@ +name: "Linux - Setup Vulkan SDK" +description: "Setup Vulkan SDK for Linux" +inputs: + path: + description: "Installation path" + required: true + version: + description: "Vulkan SDK version" + required: true + +runs: + using: "composite" + steps: + - name: Setup Vulkan SDK + id: setup + uses: ./.github/actions/unarchive-tar + with: + url: https://sdk.lunarg.com/sdk/download/${{ inputs.version }}/linux/vulkan_sdk.tar.xz + path: ${{ inputs.path }} + strip: 1 diff --git a/.github/actions/unarchive-tar/action.yml b/.github/actions/unarchive-tar/action.yml new file mode 100644 index 0000000..b97e402 --- /dev/null +++ b/.github/actions/unarchive-tar/action.yml @@ -0,0 +1,27 @@ +name: "Unarchive tar" +description: "Download and unarchive tar into directory" +inputs: + url: + description: "URL of the tar archive" + required: true + path: + description: "Directory to unarchive into" + required: true + type: + description: "Compression type (tar option)" + required: false + default: "J" + strip: + description: "Strip components" + required: false + default: "0" + +runs: + using: "composite" + steps: + - name: Unarchive into directory + shell: bash + run: | + mkdir -p ${{ inputs.path }} + cd ${{ inputs.path }} + curl --no-progress-meter ${{ inputs.url }} | tar -${{ inputs.type }}x --strip-components=${{ inputs.strip }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5ba0ee4..372921f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,9 +7,23 @@ jobs: runs-on: ubuntu-latest steps: - name: Install dependancies - run: sudo apt update; sudo apt install -y libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 libqt6multimedia6 qt6-multimedia-dev ffmpeg wget libglx-dev libgl1-mesa-dev qt6-qpa-plugins + run: | + sudo add-apt-repository -y ppa:kisak/kisak-mesa + sudo apt update + sudo apt install -y build-essential mesa-vulkan-drivers libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 libqt6multimedia6 qt6-multimedia-dev ffmpeg wget libglx-dev libgl1-mesa-dev qt6-qpa-plugins + - name: Get latest Vulkan SDK version + id: vulkan_sdk_version + run: | + echo "VULKAN_SDK_VERSION=$(curl https://vulkan.lunarg.com/sdk/latest/linux.txt)" >> "$GITHUB_ENV" + - name: Setup Vulkan SDK + uses: ./.github/actions/linux-setup-vulkan + with: + path: ./vulkan_sdk + version: ${{ env.VULKAN_SDK_VERSION }} - name: Check out repository code uses: actions/checkout@v6 + with: + submodules: 'true' - name: Install Appimagetool run: | wget -c https://github.com/$(wget -q https://github.com/probonopd/go-appimage/releases/expanded_assets/continuous -O - | grep "appimagetool-.*-x86_64.AppImage" | head -n 1 | cut -d '"' -f 2) @@ -23,8 +37,9 @@ jobs: echo "tag=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT - name: Build run: | + source ./vulkan_sdk/setup-env.sh mkdir ${{ github.workspace }}/build; cd ${{ github.workspace }}/build - cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=./install/usr -DGIT_TAG=${{ steps.version.outputs.tag }} .. + cmake -DCMAKE_BUILD_TYPE=Release -DGGML_VULKAN=ON -DCMAKE_INSTALL_PREFIX=./install/usr -DGIT_TAG=${{ steps.version.outputs.tag }} .. make -j $(nproc) make install - name: Create Appimage From e8fce10c0d5cba780efb4265fc810f7e6039a899 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 13:22:19 +0200 Subject: [PATCH 36/40] CI: checkout first --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 372921f..87146f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,10 @@ jobs: Build: runs-on: ubuntu-latest steps: + - name: Check out repository code + uses: actions/checkout@v6 + with: + submodules: 'true' - name: Install dependancies run: | sudo add-apt-repository -y ppa:kisak/kisak-mesa @@ -20,10 +24,6 @@ jobs: with: path: ./vulkan_sdk version: ${{ env.VULKAN_SDK_VERSION }} - - name: Check out repository code - uses: actions/checkout@v6 - with: - submodules: 'true' - name: Install Appimagetool run: | wget -c https://github.com/$(wget -q https://github.com/probonopd/go-appimage/releases/expanded_assets/continuous -O - | grep "appimagetool-.*-x86_64.AppImage" | head -n 1 | cut -d '"' -f 2) From 01b5b160ba3087b5a94dac00d280f7daa0d40aa4 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 13:24:44 +0200 Subject: [PATCH 37/40] CI: recursive submodule checkout --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87146f1..66e114d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ jobs: - name: Check out repository code uses: actions/checkout@v6 with: - submodules: 'true' + submodules: recursive - name: Install dependancies run: | sudo add-apt-repository -y ppa:kisak/kisak-mesa From 8762983cc7031e0ed96dd8e43706a991ccaad2c6 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 20:19:25 +0200 Subject: [PATCH 38/40] Use old style invocation to avoid bug in older qt --- src/AceStepWorker.cpp | 2 +- src/MainWindow.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AceStepWorker.cpp b/src/AceStepWorker.cpp index 8804613..0a29911 100644 --- a/src/AceStepWorker.cpp +++ b/src/AceStepWorker.cpp @@ -120,7 +120,7 @@ bool AceStepWorker::requestGeneration(SongItem song, QString requestTemplate) } // Run generation in the worker thread - QMetaObject::invokeMethod(this, &AceStepWorker::runGeneration, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "runGeneration", Qt::QueuedConnection); return true; } diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 5ed9b5a..85d9cc4 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -578,7 +578,7 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent) isGeneratingNext = true; ui->statusbar->showMessage("Generateing: "+nextSong.caption); - QMetaObject::invokeMethod(aceStep, &AceStepWorker::requestGeneration, Qt::QueuedConnection, nextSong, jsonTemplate); + aceStep->requestGeneration(nextSong, jsonTemplate); } void MainWindow::flushGenerationQueue() From 7edd7660a655398f064f504b076e2dff6b014153 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 20:53:15 +0200 Subject: [PATCH 39/40] Fix slider postion not affecting playback --- src/AudioPlayer.cpp | 2 +- src/MainWindow.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp index bb53670..46e2ee8 100644 --- a/src/AudioPlayer.cpp +++ b/src/AudioPlayer.cpp @@ -69,7 +69,7 @@ void AudioPlayer::play(std::shared_ptr audioData) buffer->setParent(this); // Use QMediaPlayer::setSourceDevice for in-memory playback - mediaPlayer->setSourceDevice(buffer, QUrl("memory://audio.wav")); + mediaPlayer->setSourceDevice(buffer); mediaPlayer->play(); // Start position timer diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 85d9cc4..f077f74 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -61,6 +61,7 @@ MainWindow::MainWindow(QWidget *parent) connect(ui->actionLoadPlaylist, &QAction::triggered, this, &MainWindow::onActionLoadPlaylist); connect(ui->actionAppendPlaylist, &QAction::triggered, this, &MainWindow::onActionAppendPlaylist); connect(ui->actionSaveSong, &QAction::triggered, this, &MainWindow::onActionSaveSong); + connect(ui->positionSlider, &QSlider::sliderMoved, this, &MainWindow::onPositionSliderSliderMoved); connect(ui->actionQuit, &QAction::triggered, this, [this]() { close(); From e4b5f9af5ebfb202e9e28f020f4a10aa57af0565 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 15 Apr 2026 21:26:41 +0200 Subject: [PATCH 40/40] Add volume slider --- src/AudioPlayer.cpp | 13 +++++++++++++ src/AudioPlayer.h | 2 ++ src/MainWindow.cpp | 16 ++++++++++++++++ src/MainWindow.h | 1 + src/MainWindow.ui | 46 +++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp index 46e2ee8..8374027 100644 --- a/src/AudioPlayer.cpp +++ b/src/AudioPlayer.cpp @@ -120,6 +120,19 @@ int AudioPlayer::position() const return mediaPlayer->position(); } +void AudioPlayer::setVolume(int volume) +{ + // Convert from 0-100 range to 0.0-1.0 range + qreal volumeLevel = static_cast(volume) / 100.0; + audioOutput->setVolume(volumeLevel); +} + +int AudioPlayer::getVolume() const +{ + // Convert from 0.0-1.0 range to 0-100 range + return static_cast(audioOutput->volume() * 100); +} + void AudioPlayer::handlePlaybackStateChanged(QMediaPlayer::PlaybackState state) { if (state == QMediaPlayer::PlayingState) diff --git a/src/AudioPlayer.h b/src/AudioPlayer.h index e1d0b47..144010f 100644 --- a/src/AudioPlayer.h +++ b/src/AudioPlayer.h @@ -32,6 +32,8 @@ public: bool isPlaying() const; int duration() const; int position() const; + void setVolume(int volume); + int getVolume() const; signals: void playbackStarted(); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index f077f74..565e8a4 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -54,6 +54,7 @@ MainWindow::MainWindow(QWidget *parent) connect(ui->skipButton, &QPushButton::clicked, this, &MainWindow::onSkipButtonClicked); connect(ui->stopButton, &QPushButton::clicked, this, &MainWindow::onStopButtonClicked); connect(ui->shuffleButton, &QPushButton::clicked, this, &MainWindow::onShuffleButtonClicked); + connect(ui->volumeSlider, &QSlider::valueChanged, this, &MainWindow::onVolumeSliderValueChanged); connect(ui->addSongButton, &QPushButton::clicked, this, &MainWindow::onAddSongButtonClicked); connect(ui->removeSongButton, &QPushButton::clicked, this, &MainWindow::onRemoveSongButtonClicked); connect(ui->actionAdvancedSettings, &QAction::triggered, this, &MainWindow::onAdvancedSettingsButtonClicked); @@ -111,6 +112,9 @@ MainWindow::MainWindow(QWidget *parent) currentSong = songModel->getSong(0); + // Set default volume (50% from UI) + audioPlayer->setVolume(ui->volumeSlider->value()); + // Start the worker thread and enter its event loop QObject::connect(&aceThread, &QThread::started, [this]() {qDebug() << "Worker thread started";}); aceThread.start(); @@ -161,6 +165,10 @@ void MainWindow::loadSettings() shuffleMode = settings.value("shuffleMode", false).toBool(); ui->shuffleButton->setChecked(shuffleMode); + // Load volume setting + int savedVolume = settings.value("volume", 50).toInt(); + ui->volumeSlider->setValue(savedVolume); + // Load path settings with defaults based on application directory QString appDir = QCoreApplication::applicationDirPath(); qwen3ModelPath = settings.value("qwen3ModelPath", @@ -189,6 +197,9 @@ void MainWindow::saveSettings() // Save shuffle mode settings.setValue("shuffleMode", shuffleMode); + // Save volume setting + settings.setValue("volume", ui->volumeSlider->value()); + // Save path settings settings.setValue("qwen3ModelPath", qwen3ModelPath); settings.setValue("textEncoderModelPath", textEncoderModelPath); @@ -548,6 +559,11 @@ void MainWindow::onPositionSliderSliderMoved(int position) } } +void MainWindow::onVolumeSliderValueChanged(int value) +{ + audioPlayer->setVolume(value); +} + void MainWindow::ensureSongsInQueue(bool enqeueCurrent) { // Only generate more songs if we're playing and not already at capacity diff --git a/src/MainWindow.h b/src/MainWindow.h index 6b3663f..25cdf9d 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -73,6 +73,7 @@ private slots: void onStopButtonClicked(); void onShuffleButtonClicked(); void onPositionSliderSliderMoved(int position); + void onVolumeSliderValueChanged(int value); void updatePosition(int position); void updateDuration(int duration); void onAddSongButtonClicked(); diff --git a/src/MainWindow.ui b/src/MainWindow.ui index 8d0a9ef..37a6f78 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -223,13 +223,55 @@ - + Qt::Orientation::Horizontal - 40 + 15 + 20 + + + + + + + + Volume: + + + + + + + + 100 + 20 + + + + 100 + + + 50 + + + Qt::Orientation::Horizontal + + + + + + + Qt::Orientation::Horizontal + + + QSizePolicy::Policy::Fixed + + + + 35 20