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/.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..66e114d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,10 +6,24 @@ 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
+ with:
+ submodules: recursive
+ - name: Install dependancies
+ 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: 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
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 c3dcea3..b1f0110 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)
@@ -7,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)
@@ -37,26 +41,29 @@ add_executable(${PROJECT_NAME}
${MusicGeneratorGUI_H}
res/resources.qrc
src/elidedlabel.h src/elidedlabel.cpp
+ src/SongItem.cpp
+
)
# 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)
@@ -66,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/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 2333021..1b7bfe9 100644
--- a/README.md
+++ b/README.md
@@ -3,36 +3,65 @@

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
+
+Grab the latest release from https://github.com/IMbackK/aceradio/releases/
+
+## Models
+
+grab the models from [here](https://huggingface.co/Serveurperso/ACE-Step-1.5-GGUF/tree/main)
## Building
-### Build acestep.cpp first:
+### Linux / macOS
-```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)
-```
+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)`
-### Build the GUI:
+### Windows (Qt6 + CMake)
-```bash
+To build on Windows 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 && cd build
-cmake ..
-make -j$(nproc)
+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.
+Go to settings->Ace Step->Model Paths and add the paths to the AceStep 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/res/xyz.uvos.aceradio.png b/res/xyz.uvos.aceradio.png
index 603d014..032045e 100644
Binary files a/res/xyz.uvos.aceradio.png and b/res/xyz.uvos.aceradio.png differ
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 @@
+
#include
#include
-#include
#include
-#include
#include
-#include
#include
-AceStep::AceStep(QObject* parent): QObject(parent)
+// acestep.cpp headers
+#include "pipeline-lm.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::isGenerateing(SongItem* song)
+AceStepWorker::~AceStepWorker()
{
- if(!busy && song)
- *song = this->request.song;
- return busy;
+ cancelGeneration();
+ unloadModels();
}
-void AceStep::cancleGenerateion()
+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)
- generationCancled(request.song);
-
- busy = false;
+ // 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;
+ 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();
+
+ // If paths changed and models are loaded, unload them so they'll be reloaded with new paths
+ if (pathsChanged && m_modelsLoaded.load())
+ {
+ unloadModels();
+ }
}
-bool AceStep::requestGeneration(SongItem song, QString requestTemplate, QString aceStepPath,
- QString qwen3ModelPath, QString textEncoderModelPath, QString ditModelPath,
- QString vaeModelPath)
+void AceStepWorker::setLowVramMode(bool enabled)
{
- if(busy)
- {
- qWarning()<<"Droping song:"<generate(), aceStepPath, textEncoderModelPath, ditModelPath, vaeModelPath};
+void AceStepWorker::setFlashAttention(bool enabled)
+{
+ m_flashAttention = enabled;
+}
- QString qwen3Binary = aceStepPath + "/ace-qwen3";
- QFileInfo qwen3Info(qwen3Binary);
- if (!qwen3Info.exists() || !qwen3Info.isExecutable())
+bool AceStepWorker::isGenerating(SongItem* song)
+{
+ if (!m_busy.load() && song)
+ *song = m_currentSong;
+ return m_busy.load();
+}
+
+void AceStepWorker::cancelGeneration()
+{
+ m_cancelRequested.store(true);
+}
+
+bool AceStepWorker::requestGeneration(SongItem song, QString requestTemplate)
+{
+ if (m_busy.load())
{
- generationError("ace-qwen3 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();
- QJsonParseError parseError;
- QJsonDocument templateDoc = QJsonDocument::fromJson(requestTemplate.toUtf8(), &parseError);
- if (!templateDoc.isObject())
+ // Validate model paths
+ if (m_lmModelPath.isEmpty() || m_textEncoderPath.isEmpty() ||
+ m_ditPath.isEmpty() || m_vaePath.isEmpty())
{
- generationError("Invalid JSON template: " + QString(parseError.errorString()));
- busy = false;
+ emit generationError("Model paths not set. Call setModelPaths() first.");
+ m_busy.store(false);
return false;
}
- 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;
-
- // Write the request file
- QFile requestFileHandle(request.requestFilePath);
- if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text))
+ // Validate model files exist
+ if (!QFileInfo::exists(m_lmModelPath))
{
- emit generationError("Failed to create request file: " + requestFileHandle.errorString());
- busy = false;
+ 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;
}
- requestFileHandle.write(QJsonDocument(requestObj).toJson(QJsonDocument::Indented));
- requestFileHandle.close();
- QStringList qwen3Args;
- qwen3Args << "--request" << request.requestFilePath;
- qwen3Args << "--model" << qwen3ModelPath;
-
- progressUpdate(30);
-
- qwenProcess.start(qwen3Binary, qwen3Args);
+ // Run generation in the worker thread
+ QMetaObject::invokeMethod(this, "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)
+ AceStepWorker* worker = static_cast(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++)
{
- QString errorOutput = qwenProcess.readAllStandardError();
- generationError("dit-vae exited with code " + QString::number(code) + ": " + errorOutput);
- busy = false;
+ 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
+ AceRequest req = songToRequest(m_currentSong, m_requestTemplate);
+ AceRequest lmOutput;
+ request_init(&lmOutput);
+
+ emit progressUpdate(10);
+
+ if (!loadLm())
+ {
+ m_busy.store(false);
return;
}
- QString ditVaeBinary = request.aceStepPath + "/dit-vae";
- QFileInfo ditVaeInfo(ditVaeBinary);
- if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable())
+ emit progressUpdate(30);
+
+ int lmResult = ace_lm_generate(m_lmContext, &req, 1, &lmOutput,
+ nullptr, nullptr,
+ checkCancel, this,
+ LM_MODE_GENERATE);
+
+ if (m_cancelRequested.load())
{
- generationError("dit-vae binary not found at: " + ditVaeBinary);
- busy = false;
+ if(m_lowVramMode)
+ unloadModels();
+ emit generationCanceled(m_currentSong);
+ m_busy.store(false);
return;
}
- request.requestLlmFilePath = tempDir + "/request_" + QString::number(request.uid) + "0.json";
- if (!QFileInfo::exists(request.requestLlmFilePath))
+ if (lmResult != 0)
{
- generationError("ace-qwen3 failed to create enhaced request file "+request.requestLlmFilePath);
- busy = false;
+ if(m_lowVramMode)
+ unloadModels();
+ emit generationError("LM generation failed or was canceled");
+ m_busy.store(false);
return;
}
- // Load lyrics from the enhanced request file
- QFile lmOutputFile(request.requestLlmFilePath);
- if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text))
+ m_currentSong.lyrics = QString::fromStdString(lmOutput.lyrics);
+
+ if(m_lowVramMode)
+ unloadLm();
+
+ 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);
+
+ 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;
+ }
+
+ 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()
+{
+ bool ret = loadSynth();
+ if(!ret)
+ return false;
+
+ ret = loadLm();
+ if(!ret)
+ return false;
+ 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);
+}
+
+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 = m_flashAttention;
+
+ 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 = m_flashAttention;
+
+ 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;
+ request_init(&req);
+
+ // Parse template first to get all default values
+ QJsonObject requestJson;
+ if (!templateJson.isEmpty())
{
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();
- }
+ QJsonDocument templateDoc = QJsonDocument::fromJson(templateJson.toUtf8(), &parseError);
+ if (templateDoc.isObject())
+ requestJson = templateDoc.object();
+ else
+ qWarning() << "Failed to parse request template:" << parseError.errorString();
}
- // Step 2: Run dit-vae to generate audio
- QStringList ditVaeArgs;
- ditVaeArgs << "--request"<(QRandomGenerator::global()->generate());
+ return req;
+}
\ No newline at end of file
diff --git a/src/AceStepWorker.h b/src/AceStepWorker.h
index 41c4676..56d95b1 100644
--- a/src/AceStepWorker.h
+++ b/src/AceStepWorker.h
@@ -1,56 +1,98 @@
+/*
+ * Copyright Carl Philipp Klemm 2026
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
#ifndef ACESTEPWORKER_H
#define ACESTEPWORKER_H
#include
#include
-#include
+#include
#include
+#include
#include "SongItem.h"
+#include "pipeline-synth.h"
+#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);
+ // Low VRAM mode: unload models between phases to save VRAM
+ 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);
signals:
void songGenerated(SongItem song);
- void generationCancled(SongItem song);
+ void generationCanceled(SongItem song);
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 isGenerateing(SongItem* song = nullptr);
- void cancleGenerateion();
-
private slots:
- void qwenProcFinished(int code, QProcess::ExitStatus status);
- void ditProcFinished(int code, QProcess::ExitStatus status);
+ void runGeneration();
+
+private:
+ static bool checkCancel(void* data);
+
+ bool loadModels();
+ void unloadModels();
+ bool loadLm();
+ void unloadLm();
+ bool loadSynth();
+ void unloadSynth();
+
+ AceRequest songToRequest(const SongItem& song, const QString& templateJson);
+
+ static std::shared_ptr convertToWav(const AceAudio& audio);
+
+ // Generation state
+ std::atomic m_busy{false};
+ std::atomic m_cancelRequested{false};
+ std::atomic m_modelsLoaded{false};
+ bool m_lowVramMode = false;
+ bool m_flashAttention = true;
+
+ // 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;
+
+ QByteArray m_lmModelPathBytes;
+ QByteArray m_textEncoderPathBytes;
+ QByteArray m_ditPathBytes;
+ QByteArray m_vaePathBytes;
};
-#endif // ACESTEPWORKER_H
+#endif // ACESTEPWORKER_H
\ No newline at end of file
diff --git a/src/AdvancedSettingsDialog.cpp b/src/AdvancedSettingsDialog.cpp
index 163ba8a..7e2e412 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
@@ -10,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()
@@ -22,11 +31,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();
@@ -47,16 +51,21 @@ QString AdvancedSettingsDialog::getVAEModelPath() const
return ui->vaeModelEdit->text();
}
+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);
}
-void AdvancedSettingsDialog::setAceStepPath(const QString &path)
-{
- ui->aceStepPathEdit->setText(path);
-}
-
void AdvancedSettingsDialog::setQwen3ModelPath(const QString &path)
{
ui->qwen3ModelEdit->setText(path);
@@ -77,16 +86,17 @@ void AdvancedSettingsDialog::setVAEModelPath(const QString &path)
ui->vaeModelEdit->setText(path);
}
-void AdvancedSettingsDialog::on_aceStepBrowseButton_clicked()
+void AdvancedSettingsDialog::setLowVramMode(bool enabled)
{
- QString dir = QFileDialog::getExistingDirectory(this, "Select AceStep Build Directory", ui->aceStepPathEdit->text());
- if (!dir.isEmpty())
- {
- ui->aceStepPathEdit->setText(dir);
- }
+ ui->lowVramCheckBox->setChecked(enabled);
}
-void AdvancedSettingsDialog::on_qwen3BrowseButton_clicked()
+void AdvancedSettingsDialog::setFlashAttention(bool enabled)
+{
+ ui->flashAttentionCheckBox->setChecked(enabled);
+}
+
+void AdvancedSettingsDialog::onQwen3BrowseButtonClicked()
{
QString file = QFileDialog::getOpenFileName(this, "Select Qwen3 Model", ui->qwen3ModelEdit->text(),
"GGUF Files (*.gguf)");
@@ -96,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)");
@@ -106,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())
@@ -115,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 fb4aa69..8a3047a 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
@@ -19,26 +24,27 @@ public:
// Getters for settings
QString getJsonTemplate() const;
- QString getAceStepPath() const;
QString getQwen3ModelPath() const;
QString getTextEncoderModelPath() const;
QString getDiTModelPath() const;
QString getVAEModelPath() const;
+ bool getLowVramMode() const;
+ bool getFlashAttention() const;
// 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);
void setVAEModelPath(const QString &path);
+ void setLowVramMode(bool enabled);
+ void setFlashAttention(bool enabled);
private slots:
- void on_aceStepBrowseButton_clicked();
- 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/AdvancedSettingsDialog.ui b/src/AdvancedSettingsDialog.ui
index e9fadf1..6e95e90 100644
--- a/src/AdvancedSettingsDialog.ui
+++ b/src/AdvancedSettingsDialog.ui
@@ -1,203 +1,239 @@
-
- AdvancedSettingsDialog
-
-
-
- 0
- 0
- 600
- 450
-
-
-
- Advanced Settings
-
-
- -
-
-
- 0
-
-
-
- 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
+
+ -
+
+
+ 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/AudioPlayer.cpp b/src/AudioPlayer.cpp
index 76694d4..8374027 100644
--- a/src/AudioPlayer.cpp
+++ b/src/AudioPlayer.cpp
@@ -1,4 +1,8 @@
+// Copyright Carl Philipp Klemm 2026
+// SPDX-License-Identifier: GPL-3.0-or-later
+
#include "AudioPlayer.h"
+#include
#include
AudioPlayer::AudioPlayer(QObject *parent)
@@ -45,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);
+ mediaPlayer->play();
+
+ // Start position timer
+ positionTimer->start();
+}
+
void AudioPlayer::play()
{
if (!isPlaying())
@@ -89,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 93f14b2..144010f 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
@@ -9,6 +14,7 @@
#include
#include
#include
+#include
class AudioPlayer : public QObject
{
@@ -18,6 +24,7 @@ public:
~AudioPlayer();
void play(const QString &filePath);
+ void play(std::shared_ptr audioData);
void play();
void stop();
void pause();
@@ -25,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 62c8a63..565e8a4 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"
@@ -18,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),
playbackTimer(new QTimer(this)),
isPlaying(false),
isPaused(false),
@@ -26,6 +29,7 @@ MainWindow::MainWindow(QWidget *parent)
isGeneratingNext(false)
{
aceStep->moveToThread(&aceThread);
+ aceThread.setObjectName("AceStep Woker Thread");
ui->setupUi(this);
@@ -38,15 +42,27 @@ 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();
// 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->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);
+ 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->positionSlider, &QSlider::sliderMoved, this, &MainWindow::onPositionSliderSliderMoved);
connect(ui->actionQuit, &QAction::triggered, this, [this]()
{
close();
@@ -59,16 +75,16 @@ 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::generationCancled, 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);
+ connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::onSongListViewDoubleClicked);
// 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);
});
@@ -95,11 +111,20 @@ MainWindow::MainWindow(QWidget *parent)
ui->nowPlayingLabel->setText("Now Playing:");
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();
}
MainWindow::~MainWindow()
{
- aceStep->cancleGenerateion();
+ aceStep->cancelGeneration();
+ aceThread.quit();
+ aceThread.wait();
autoSavePlaylist();
saveSettings();
@@ -140,15 +165,26 @@ 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();
- 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",
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);
+
+ // Load flash attention setting
+ bool flashAttention = settings.value("flashAttention", false).toBool();
+ aceStep->setFlashAttention(flashAttention);
}
void MainWindow::saveSettings()
@@ -161,13 +197,21 @@ void MainWindow::saveSettings()
// Save shuffle mode
settings.setValue("shuffleMode", shuffleMode);
+ // Save volume setting
+ settings.setValue("volume", ui->volumeSlider->value());
+
// Save path settings
- settings.setValue("aceStepPath", aceStepPath);
settings.setValue("qwen3ModelPath", qwen3ModelPath);
settings.setValue("textEncoderModelPath", textEncoderModelPath);
settings.setValue("ditModelPath", ditModelPath);
settings.setValue("vaeModelPath", vaeModelPath);
+ // Save low VRAM mode
+ settings.setValue("lowVramMode", aceStep->isLowVramMode());
+
+ // Save flash attention setting
+ settings.setValue("flashAttention", aceStep->isFlashAttention());
+
settings.setValue("firstRun", false);
}
@@ -216,7 +260,7 @@ void MainWindow::updateControls()
ui->removeSongButton->setEnabled(hasSongs && ui->songListView->currentIndex().isValid());
}
-void MainWindow::on_playButton_clicked()
+void MainWindow::onPlayButtonClicked()
{
if (isPaused)
{
@@ -237,9 +281,9 @@ void MainWindow::on_playButton_clicked()
updateControls();
}
-void MainWindow::on_pauseButton_clicked()
+void MainWindow::onPauseButtonClicked()
{
- if (isPlaying && !isPaused)
+ if (isPlaying && !isPaused && audioPlayer->isPlaying())
{
// Pause playback
audioPlayer->pause();
@@ -248,7 +292,7 @@ void MainWindow::on_pauseButton_clicked()
}
}
-void MainWindow::on_skipButton_clicked()
+void MainWindow::onSkipButtonClicked()
{
if (isPlaying)
{
@@ -258,7 +302,7 @@ void MainWindow::on_skipButton_clicked()
}
}
-void MainWindow::on_stopButton_clicked()
+void MainWindow::onStopButtonClicked()
{
if (isPlaying)
{
@@ -272,7 +316,7 @@ void MainWindow::on_stopButton_clicked()
}
}
-void MainWindow::on_shuffleButton_clicked()
+void MainWindow::onShuffleButtonClicked()
{
shuffleMode = ui->shuffleButton->isChecked();
updateControls();
@@ -282,18 +326,13 @@ void MainWindow::on_shuffleButton_clicked()
ensureSongsInQueue();
}
-void MainWindow::on_addSongButton_clicked()
+void MainWindow::onAddSongButtonClicked()
{
SongDialog dialog(this);
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
@@ -302,17 +341,18 @@ 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();
if (index.column() == 0)
{
+ isPaused = false;
if (isPlaying)
{
audioPlayer->stop();
@@ -320,8 +360,8 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
else
{
isPlaying = true;
- updateControls();
}
+ updateControls();
flushGenerationQueue();
ui->nowPlayingLabel->setText("Now Playing: Waiting for generation...");
@@ -332,24 +372,16 @@ 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);
+ connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::onSongListViewDoubleClicked);
}
-void MainWindow::on_removeSongButton_clicked()
+void MainWindow::onRemoveSongButtonClicked()
{
QModelIndex currentIndex = ui->songListView->currentIndex();
if (!currentIndex.isValid())
@@ -369,17 +401,18 @@ void MainWindow::on_removeSongButton_clicked()
}
}
-void MainWindow::on_advancedSettingsButton_clicked()
+void MainWindow::onAdvancedSettingsButtonClicked()
{
AdvancedSettingsDialog dialog(this);
// Set current values
dialog.setJsonTemplate(jsonTemplate);
- dialog.setAceStepPath(aceStepPath);
dialog.setQwen3ModelPath(qwen3ModelPath);
dialog.setTextEncoderModelPath(textEncoderModelPath);
dialog.setDiTModelPath(ditModelPath);
dialog.setVAEModelPath(vaeModelPath);
+ dialog.setLowVramMode(aceStep->isLowVramMode());
+ dialog.setFlashAttention(aceStep->isFlashAttention());
if (dialog.exec() == QDialog::Accepted)
{
@@ -394,12 +427,20 @@ void MainWindow::on_advancedSettingsButton_clicked()
// Update settings
jsonTemplate = dialog.getJsonTemplate();
- aceStepPath = dialog.getAceStepPath();
qwen3ModelPath = dialog.getQwen3ModelPath();
textEncoderModelPath = dialog.getTextEncoderModelPath();
ditModelPath = dialog.getDiTModelPath();
vaeModelPath = dialog.getVAEModelPath();
+ // Update model paths for acestep.cpp
+ aceStep->setModelPaths(qwen3ModelPath, textEncoderModelPath, ditModelPath, vaeModelPath);
+
+ // Update low VRAM mode
+ aceStep->setLowVramMode(dialog.getLowVramMode());
+
+ // Update flash attention setting
+ aceStep->setFlashAttention(dialog.getFlashAttention());
+
saveSettings();
}
}
@@ -412,11 +453,19 @@ 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);
ui->jsonTextEdit->setPlainText(song.json);
+ updateControls();
}
void MainWindow::songGenerated(const SongItem& song)
@@ -502,7 +551,7 @@ void MainWindow::updatePlaybackStatus(bool playing)
updateControls();
}
-void MainWindow::on_positionSlider_sliderMoved(int position)
+void MainWindow::onPositionSliderSliderMoved(int position)
{
if (isPlaying && audioPlayer->isPlaying())
{
@@ -510,6 +559,11 @@ void MainWindow::on_positionSlider_sliderMoved(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
@@ -520,7 +574,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();
@@ -541,21 +595,18 @@ 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()
{
generatedSongQueue.clear();
- aceStep->cancleGenerateion();
+ aceStep->cancelGeneration();
isGeneratingNext = false;
}
// Playlist save/load methods
-void MainWindow::on_actionSavePlaylist()
+void MainWindow::onActionSavePlaylist()
{
QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist",
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/playlist.json",
@@ -567,7 +618,7 @@ void MainWindow::on_actionSavePlaylist()
}
}
-void MainWindow::on_actionLoadPlaylist()
+void MainWindow::onActionLoadPlaylist()
{
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
@@ -580,7 +631,7 @@ void MainWindow::on_actionLoadPlaylist()
}
}
-void MainWindow::on_actionAppendPlaylist()
+void MainWindow::onActionAppendPlaylist()
{
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
@@ -591,7 +642,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",
@@ -615,7 +666,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();
}
@@ -692,16 +758,13 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList(song.uniqueId); // Store as qint64 for JSON compatibility
+ 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();
@@ -750,8 +813,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;
@@ -773,35 +835,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();
- }
-
- // 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();
- }
-
+ SongItem song(songObj);
songs.append(song);
}
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 54136f7..25cdf9d 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
@@ -31,7 +36,7 @@ class MainWindow : public QMainWindow
SongListModel *songModel;
AudioPlayer *audioPlayer;
QThread aceThread;
- AceStep *aceStep;
+ AceStepWorker *aceStep;
QTimer *playbackTimer;
QString formatTime(int milliseconds);
@@ -45,7 +50,6 @@ class MainWindow : public QMainWindow
QString jsonTemplate;
// Path settings
- QString aceStepPath;
QString qwen3ModelPath;
QString textEncoderModelPath;
QString ditModelPath;
@@ -63,19 +67,20 @@ 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 onVolumeSliderValueChanged(int value);
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);
@@ -84,10 +89,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/MainWindow.ui b/src/MainWindow.ui
index 828189b..37a6f78 100644
--- a/src/MainWindow.ui
+++ b/src/MainWindow.ui
@@ -178,6 +178,9 @@
-
+
+ false
+
Pause
@@ -220,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
diff --git a/src/SongDialog.cpp b/src/SongDialog.cpp
index baa9ddc..e0ebde8 100644
--- a/src/SongDialog.cpp
+++ b/src/SongDialog.cpp
@@ -1,48 +1,142 @@
+// Copyright Carl Philipp Klemm 2026
+// SPDX-License-Identifier: GPL-3.0-or-later
+
#include "SongDialog.h"
#include "ui_SongDialog.h"
#include
-SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &lyrics, const QString &vocalLanguage)
+SongDialog::SongDialog(QWidget *parent, const SongItem &song)
: QDialog(parent),
- ui(new Ui::SongDialog)
+ song(song),
+ 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);
- }
+ // 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);
// Setup vocal language combo box
- ui->vocalLanguageCombo->addItem("--", ""); // Unset
+ ui->vocalLanguageCombo->addItem("--", "");
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())
+ 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()
@@ -50,25 +144,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();
-}
-
-void SongDialog::on_okButton_clicked()
+void SongDialog::onOkButtonClicked()
{
// 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.");
@@ -78,7 +157,22 @@ void SongDialog::on_okButton_clicked()
accept();
}
-void SongDialog::on_cancelButton_clicked()
+void SongDialog::onCancelButtonClicked()
{
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 c8137f5..5e4ce48 100644
--- a/src/SongDialog.h
+++ b/src/SongDialog.h
@@ -1,9 +1,16 @@
+/*
+ * Copyright Carl Philipp Klemm 2026
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
#ifndef SONGDIALOG_H
#define SONGDIALOG_H
#include
#include
+#include "SongItem.h"
+
namespace Ui
{
class SongDialog;
@@ -12,19 +19,17 @@ 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 = "");
+ explicit SongDialog(QWidget *parent = nullptr, const SongItem& song = SongItem());
~SongDialog();
- QString getCaption() const;
- QString getLyrics() const;
- QString getVocalLanguage() const;
+ const SongItem& getSong();
private slots:
- void on_okButton_clicked();
- void on_cancelButton_clicked();
+ void onOkButtonClicked();
+ void onCancelButtonClicked();
private:
Ui::SongDialog *ui;
diff --git a/src/SongDialog.ui b/src/SongDialog.ui
index efedda8..56e0842 100644
--- a/src/SongDialog.ui
+++ b/src/SongDialog.ui
@@ -1,109 +1,157 @@
-
- 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
+
+
+
+ -
+
+
+ -
+
+
+ 5
+
+
-
+
+
+ Keyscale
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Vocal Language:
+
+
+ Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Bpm (0 for llm chooses)
+
+
+
+ -
+
+
+ 9999
+
+
+
+
+
+ -
+
+
+ 5
+
+
+ 0
+
+
+
+ -
+
+
-
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ OK
+
+
+
+ -
+
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SongItem.cpp b/src/SongItem.cpp
new file mode 100644
index 0000000..4b1686e
--- /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), bpm(0)
+{
+ 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 8a28891..0a80926 100644
--- a/src/SongItem.h
+++ b/src/SongItem.h
@@ -1,22 +1,33 @@
+/*
+ * Copyright Carl Philipp Klemm 2026
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
#pragma once
#include
#include
#include
+#include
+#include
class SongItem
{
public:
QString caption;
QString lyrics;
+ unsigned int bpm;
+ QString key;
+ QString vocalLanguage;
+ bool cotCaption;
+
uint64_t uniqueId;
QString file;
- QString vocalLanguage;
QString json;
+ std::shared_ptr audioData;
- inline SongItem(const QString &caption = "", const QString &lyrics = "")
- : caption(caption), lyrics(lyrics)
- {
- // Generate a unique ID using 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 caa856a..0b40f05 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
@@ -108,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 4a3275d..bf1e22e 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
@@ -32,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
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
diff --git a/tests/test_acestep_worker.cpp b/tests/test_acestep_worker.cpp
new file mode 100644
index 0000000..60a5ffa
--- /dev/null
+++ b/tests/test_acestep_worker.cpp
@@ -0,0 +1,525 @@
+// 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.audioData != nullptr);
+ ASSERT_TRUE(!resultSong.audioData->isEmpty());
+
+ // 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
+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);
+}
+
+// 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);
+
+ 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);
+ RUN_TEST(generateSongLowVram);
+ RUN_TEST(normalModeKeepsModelsLoaded);
+ RUN_TEST(lowVramModeToggle);
+
+ 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