├── .gitignore ├── icons ├── composer.png ├── edit-copy.png ├── edit-cut.png ├── edit-redo.png ├── edit-undo.png ├── help-hint.png ├── zoom-in.png ├── zoom-out.png ├── edit-delete.png ├── edit-paste.png ├── help-about.png ├── insert-text.png ├── document-new.png ├── document-open.png ├── document-save.png ├── help-contents.png ├── insert-object.png ├── zoom-original.png ├── application-exit.png ├── document-save-as.png ├── edit-select-all.png ├── media-playback-stop.png ├── preferences-other.png ├── media-playback-pause.png └── media-playback-start.png ├── .github ├── PULL_REQUEST_TEMPLATE │ ├── mergeback.md │ └── release.md ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── PULL_REQUEST_TEMPLATE.md ├── CONTRIBUTING.md ├── workflows │ ├── build_functions.sh │ ├── comment_on_pr.yml │ ├── appimage.yml │ └── build_and_release.yml └── CODE_OF_CONDUCT.md ├── docs ├── Authors.txt ├── FileFormats.txt ├── Design.txt └── helpindex.html ├── platform ├── composer.desktop ├── mingw-cross-env │ ├── pathconfig.sh │ ├── makebuilddir.sh │ ├── makezip.sh │ ├── README │ ├── mk │ │ ├── ffmpeg.mk │ │ └── qt.mk │ ├── makeinstaller.py │ └── copydlls.py └── packaging.cmake ├── src ├── operation.cc ├── types.hh ├── scrollbar.hh ├── config.cmake.hh ├── busydialog.hh ├── songwriter-smm.cc ├── util.hh ├── scrollbar.cc ├── songwriter.hh ├── gettingstarted.hh ├── songwriter-lrc.cc ├── pitchvis.hh ├── CMakeLists.txt ├── main.cc ├── songwriter-txt.cc ├── synth.hh ├── notes.cc ├── operation.hh ├── notelabel.hh ├── songparser.hh ├── song.cc ├── libda │ ├── sample.hpp │ ├── fft.hpp │ └── portaudio.hpp ├── notes.hh ├── textcodecselector.hh ├── songparser-lrc.cc ├── songparser-xml.cc ├── ffmpeg.hh ├── songwriter-ini.cc ├── pitch.hh ├── midifile.hh ├── songwriter-xml.cc ├── editorapp.hh ├── songparser-txt.cc ├── songparser.cc ├── notegraphwidget.hh └── song.hh ├── docker ├── Dockerfile.fedora ├── Dockerfile.ubuntu ├── Dockerfile.debian ├── README.md └── build_composer.sh ├── editor.qrc ├── cmake ├── FindAVUtil.cmake ├── FindSWScale.cmake ├── FindAVFormat.cmake ├── FindAVCodec.cmake ├── FindSWResample.cmake └── Copyright.txt ├── CMakeLists.txt ├── AppImageBuilder.yml ├── CMakePresets.json ├── README.md └── ui └── aboutdialog.ui /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | build32 3 | build64 4 | CMakeLists.txt.user* 5 | .vs -------------------------------------------------------------------------------- /icons/composer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/composer.png -------------------------------------------------------------------------------- /icons/edit-copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/edit-copy.png -------------------------------------------------------------------------------- /icons/edit-cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/edit-cut.png -------------------------------------------------------------------------------- /icons/edit-redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/edit-redo.png -------------------------------------------------------------------------------- /icons/edit-undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/edit-undo.png -------------------------------------------------------------------------------- /icons/help-hint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/help-hint.png -------------------------------------------------------------------------------- /icons/zoom-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/zoom-in.png -------------------------------------------------------------------------------- /icons/zoom-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/zoom-out.png -------------------------------------------------------------------------------- /icons/edit-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/edit-delete.png -------------------------------------------------------------------------------- /icons/edit-paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/edit-paste.png -------------------------------------------------------------------------------- /icons/help-about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/help-about.png -------------------------------------------------------------------------------- /icons/insert-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/insert-text.png -------------------------------------------------------------------------------- /icons/document-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/document-new.png -------------------------------------------------------------------------------- /icons/document-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/document-open.png -------------------------------------------------------------------------------- /icons/document-save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/document-save.png -------------------------------------------------------------------------------- /icons/help-contents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/help-contents.png -------------------------------------------------------------------------------- /icons/insert-object.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/insert-object.png -------------------------------------------------------------------------------- /icons/zoom-original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/zoom-original.png -------------------------------------------------------------------------------- /icons/application-exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/application-exit.png -------------------------------------------------------------------------------- /icons/document-save-as.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/document-save-as.png -------------------------------------------------------------------------------- /icons/edit-select-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/edit-select-all.png -------------------------------------------------------------------------------- /icons/media-playback-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/media-playback-stop.png -------------------------------------------------------------------------------- /icons/preferences-other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/preferences-other.png -------------------------------------------------------------------------------- /icons/media-playback-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/media-playback-pause.png -------------------------------------------------------------------------------- /icons/media-playback-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performous/composer/HEAD/icons/media-playback-start.png -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/mergeback.md: -------------------------------------------------------------------------------- 1 | ### What does this PR do? 2 | 3 | Merge v{{.Version}} into master 4 | 5 | ### Motivation 6 | 7 | Be sync. 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/release.md: -------------------------------------------------------------------------------- 1 | ### What does this PR do? 2 | 3 | Prepare release v{{.Version}}. 4 | 5 | ### Motivation 6 | 7 | Create a new release. 8 | -------------------------------------------------------------------------------- /docs/Authors.txt: -------------------------------------------------------------------------------- 1 | Primary Authors: 2 | Lasse Kärkkäinen 3 | Tapio Vierros 4 | 5 | See Git repository history for full list of contributors. 6 | 7 | This applications uses code from the Performous game. 8 | 9 | -------------------------------------------------------------------------------- /platform/composer.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Name=Composer 4 | Comment=Note creator for pitch-detecting karaoke games 5 | Exec=composer 6 | Icon=composer.png 7 | Terminal=false 8 | Type=Application 9 | Categories=Application;AudioVideo;Music;Qt; 10 | 11 | -------------------------------------------------------------------------------- /src/operation.cc: -------------------------------------------------------------------------------- 1 | #include "operation.hh" 2 | 3 | 4 | QDataStream& operator<<(QDataStream& stream, const Operation& op) 5 | { 6 | stream << op.m_params; 7 | return stream; 8 | } 9 | 10 | QDataStream& operator>>(QDataStream& stream, Operation& op) 11 | { 12 | stream >> op.m_params; 13 | return stream; 14 | } 15 | -------------------------------------------------------------------------------- /src/types.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef WIN32 4 | 5 | typedef signed char int8_t; 6 | typedef unsigned char uint8_t; 7 | typedef short int16_t; 8 | typedef unsigned short uint16_t; 9 | typedef int int32_t; 10 | typedef unsigned int uint32_t; 11 | 12 | #else 13 | 14 | #include 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Wiki 4 | url: "https://github.com/performous/composer/wiki/" 5 | about: "Consult the wiki first, it might already contain the information you are looking for" 6 | 7 | - name: Question 8 | url: "https://github.com/performous/composer/discussions" 9 | about: "Please ask questions related to usage/setup/support/non-issue development discussion in the Discussions section" 10 | 11 | - name: Question 12 | url: "https://discord.gg/NS3m3ad" 13 | about: "Alternatively, ask on Discord in the channel user_support" -------------------------------------------------------------------------------- /platform/mingw-cross-env/pathconfig.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is sourced by the other scripts to determine some paths. 3 | # Feel free to modify the variables at the top of this file. 4 | 5 | SOURCE_DIR="`pwd`/../.." 6 | BUILD_DIR="`pwd`/build" 7 | INSTALL_DIR="$BUILD_DIR/stage" 8 | 9 | 10 | # You shouldn't need to touch anything below this 11 | 12 | CROSS_ID=i686-pc-mingw32 13 | 14 | CROSS_CXX=`which ${CROSS_ID}"-g++"` 15 | 16 | if [ $? -ne 0 ]; then 17 | echo "Couldn't find cross-compiler ${CROSS_ID}-g++!" 18 | echo "Please add it to your PATH." 19 | exit 1 20 | fi 21 | 22 | CROSS_PREFIX=`dirname "$CROSS_CXX"`/.. 23 | CROSS_PREFIX=`readlink -f "$CROSS_PREFIX"` 24 | 25 | -------------------------------------------------------------------------------- /src/scrollbar.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class ScrollBar : public QScrollBar 8 | { 9 | public: 10 | using PaintFunction = std::function; 11 | 12 | ScrollBar(Qt::Orientation orientation, QWidget * parent = nullptr); 13 | ScrollBar(PaintFunction const&, Qt::Orientation orientation, QWidget * parent = nullptr); 14 | 15 | void update(); 16 | 17 | protected: 18 | void paintEvent(QPaintEvent *) override; 19 | void resizeEvent(QResizeEvent * event) override; 20 | 21 | private: 22 | void paint(); 23 | 24 | private: 25 | PaintFunction m_paintFunction; 26 | QImage m_background; 27 | }; 28 | -------------------------------------------------------------------------------- /platform/mingw-cross-env/makebuilddir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Creates a build dir and does the required CMake mangling. 3 | 4 | source pathconfig.sh 5 | 6 | mkdir -p "$BUILD_DIR" 7 | cd "$BUILD_DIR" 8 | cat > Toolchain.cmake << EOF 9 | set(CMAKE_SYSTEM_NAME Windows) 10 | set(CMAKE_C_COMPILER ${CROSS_ID}-gcc) 11 | set(CMAKE_CXX_COMPILER ${CROSS_ID}-g++) 12 | set(CMAKE_FIND_ROOT_PATH ${CROSS_PREFIX}/${CROSS_ID}) 13 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 14 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 15 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 16 | set(WINDRES ${CROSS_ID}-windres) 17 | EOF 18 | 19 | PATH="${CROSS_PREFIX}/bin:$PATH" cmake -DCMAKE_TOOLCHAIN_FILE=Toolchain.cmake -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" "$SOURCE_DIR" 20 | 21 | -------------------------------------------------------------------------------- /platform/mingw-cross-env/makezip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # Creates a simple zip package. 3 | 4 | source pathconfig.sh 5 | 6 | ARCHIVE=editor.7z 7 | 8 | ARCHIVER=`which 7z` 9 | if [ $? -ne 0 ]; then 10 | echo "Couldn't find 7z, cannot create archive." 11 | exit 1 12 | fi 13 | 14 | cd "$BUILD_DIR" 15 | rm -rf "$INSTALL_DIR" 16 | mkdir -p "$INSTALL_DIR" 17 | 18 | # No install target yet 19 | cp *.exe "$INSTALL_DIR" 20 | 21 | # Copy DLLs 22 | ../copydlls.py "$CROSS_PREFIX/$CROSS_ID/bin" "$INSTALL_DIR" 23 | 24 | # These ones are not detected by copydlls.py 25 | cp "$CROSS_PREFIX/$CROSS_ID/bin/QtSvg4.dll" "$INSTALL_DIR" 26 | cp -r "$CROSS_PREFIX/$CROSS_ID/plugins/phonon_backend" "$INSTALL_DIR" 27 | 28 | # Create archive 29 | cd "$INSTALL_DIR" 30 | $ARCHIVER a -bd -r "$BUILD_DIR/$ARCHIVE" * 31 | 32 | -------------------------------------------------------------------------------- /docker/Dockerfile.fedora: -------------------------------------------------------------------------------- 1 | ARG OS_VERSION 2 | ## Use the official Fedora Image from Dockerhub 3 | FROM docker.io/library/fedora:${OS_VERSION} 4 | 5 | ## Install the deps and create the working directory 6 | ## Enable the RPM Fusion Free Repo 7 | RUN dnf install -y https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm &&\ 8 | dnf install -y git cmake gcc-c++ gettext ffmpeg-devel \ 9 | help2man redhat-lsb rpm-build qt5-qtbase-devel qt5-qtmultimedia-devel && \ 10 | dnf clean all && \ 11 | mkdir /root/composer 12 | 13 | ## Copy in the build script to make things easy 14 | COPY build_composer.sh /root/composer/build_composer.sh 15 | 16 | WORKDIR /root/composer 17 | -------------------------------------------------------------------------------- /docker/Dockerfile.ubuntu: -------------------------------------------------------------------------------- 1 | ARG OS_VERSION 2 | ## Use the official Ubuntu Image from Dockerhub 3 | FROM docker.io/library/ubuntu:${OS_VERSION} 4 | 5 | ## Set up environment variables so the tzdata install doesn't 6 | ## hang on asking for user input for configuration 7 | ARG DEBIAN_FRONTEND="noninteractive" 8 | ARG TZ="America/New_York" 9 | 10 | ## Install the deps and create the build directory 11 | RUN apt-get update && \ 12 | apt-get install -y --no-install-recommends git cmake build-essential \ 13 | gettext help2man libavcodec-dev libavformat-dev libswscale-dev \ 14 | qtbase5-dev qtmultimedia5-dev ca-certificates file && \ 15 | apt-get clean && \ 16 | mkdir /root/composer 17 | 18 | ## Copy in the build script to make things easy 19 | COPY build_composer.sh /root/composer/build_composer.sh 20 | 21 | WORKDIR /root/composer 22 | -------------------------------------------------------------------------------- /src/config.cmake.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // CMake uses config.cmake.hh to generate config.hh within the build folder. 4 | #ifndef EDITOR_CONFIG_HH 5 | #define EDITOR_CONFIG_HH 6 | 7 | #define PACKAGE "@CMAKE_PROJECT_NAME@" 8 | #define VERSION "@PROJECT_VERSION@" 9 | 10 | #cmakedefine STATIC_PLUGINS 11 | 12 | //#define SHARED_DATA_DIR "@SHARE_INSTALL@" 13 | 14 | // FFMPEG libraries use changing include file names... Get them from CMake. 15 | #define AVCODEC_INCLUDE <@AVCodec_INCLUDE@> 16 | #define AVCODEC_CODEC_PAR_INCLUDE 17 | #define AVFORMAT_INCLUDE <@AVFormat_INCLUDE@> 18 | #define SWRESAMPLE_INCLUDE <@SWResample_INCLUDE@> 19 | #define SWSCALE_INCLUDE <@SWScale_INCLUDE@> 20 | #define AVUTIL_INCLUDE <@AVUtil_INCLUDE@> 21 | #define AVUTIL_OPT_INCLUDE //HACK to get AVOption class! 22 | #define AVUTIL_MATH_INCLUDE 23 | #define AVUTIL_ERROR_INCLUDE 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | ### What does this PR do? 18 | 19 | 20 | 21 | ### Closes Issue(s) 22 | 23 | 24 | 25 | ### Motivation 26 | 27 | 28 | 29 | 30 | ### More 31 | 32 | - [ ] Added/updated documentation 33 | 34 | ### Additional Notes 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/busydialog.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class BusyDialog: public QDialog { 10 | public: 11 | BusyDialog(QWidget *parent = NULL, int eventsInterval = 10): QDialog(parent), timer(), interval(eventsInterval), count() { 12 | QProgressBar *progress = new QProgressBar(this); 13 | progress->setRange(0,0); 14 | setWindowTitle(tr("Working...")); 15 | QVBoxLayout *vb = new QVBoxLayout(this); 16 | vb->addWidget(progress); 17 | setLayout(vb); 18 | timer.start(); 19 | } 20 | void operator()() { 21 | // Only show the dialog after certainamount of time 22 | if (isHidden() && timer.elapsed() > 3000) open(); 23 | if (isVisible()) { 24 | if (count == 0) // Let's not process events all the time 25 | QApplication::processEvents(); 26 | count = (count + 1) % interval; 27 | } 28 | } 29 | protected: 30 | void closeEvent(QCloseEvent* event) { event->ignore(); } 31 | private: 32 | QElapsedTimer timer; 33 | int interval; 34 | int count; 35 | }; 36 | -------------------------------------------------------------------------------- /src/songwriter-smm.cc: -------------------------------------------------------------------------------- 1 | #include "songwriter.hh" 2 | #include "config.hh" 3 | #include "util.hh" 4 | #include 5 | #include 6 | 7 | void SMMWriter::writeSMM() const { 8 | QFile f(path + "/" + s.artist + " - " + s.title + ".txt"); 9 | if (!f.open(QFile::WriteOnly | QFile::Truncate)) 10 | throw std::runtime_error("Couldn't open target file"); 11 | 12 | QTextStream out(&f); 13 | out.setCodec("UTF-8"); 14 | 15 | // Loop through the notes 16 | const Notes& notes = s.getVocalTrack().notes; 17 | for (int i = 0; i < notes.size(); ++i) { 18 | const Note& n = notes[i]; 19 | if (i == 0 || n.lineBreak) 20 | out << '\n' << sec2timestamp(n.begin); 21 | // Put timestamp between notes 22 | out << n.syllable << sec2timestamp(n.begin); 23 | if (n.type == Note::SLEEP) 24 | out << '\n'; 25 | } 26 | out << '\n'; 27 | } 28 | 29 | 30 | 31 | QString SMMWriter::sec2timestamp(double sec) const { 32 | double modsec = std::fmod(sec, 60.0); 33 | return QString("[%1:%2:%3]") 34 | .arg(int(sec/60), 2, 10, QChar('0')) 35 | .arg(int(modsec), 2, 10, QChar('0')) 36 | .arg(int(100 * (modsec - int(modsec))), 2, 10, QChar('0')); 37 | } 38 | -------------------------------------------------------------------------------- /platform/mingw-cross-env/README: -------------------------------------------------------------------------------- 1 | How to cross-compile from Linux for Windows 2 | =========================================== 3 | 4 | 1. Download and extract mingw-cross-env from http://mingw-cross-env.nongnu.org/ to your desired location. 5 | 6 | 2. Read through http://mingw-cross-env.nongnu.org/#usage to learn how to utilize all your processor cores for speedier compilation. 7 | 8 | 3. Compile GCC and its dependencies: make gcc 9 | 10 | 4. Replace ffmpeg.mk and qt.mk files in the src-subfolder of your cross-env installation with those provided in the mk-directory here. This enables Phonon and uses shared libraries instead of static ones (static libs cause trouble in linking and apparantly Phonon cannot be even built that way). 11 | 12 | 5. Compile ffmpeg and qt: make ffmpeg qt 13 | 14 | 6. Add the cross-compiler's bin directory (e.g. $HOME/mingw/usr/bin) to your PATH environment variable. 15 | 16 | 7. Run makebuilddir.sh 17 | 18 | 8. Compile editor as usual: cd build && make 19 | 20 | 9. Run makezip.sh if you want to create a simple zip package. 21 | 22 | 10. To create an executable installer, run makeinstaller.py from the build dir (requires makezip.sh to be run first) 23 | -------------------------------------------------------------------------------- /docker/Dockerfile.debian: -------------------------------------------------------------------------------- 1 | ARG OS_VERSION 2 | ## Use the official Debian Image from Dockerhub 3 | FROM docker.io/library/debian:${OS_VERSION} 4 | 5 | ## Copy OS_VERSION into ENV so we can use it in scripts too 6 | ARG OS_VERSION 7 | ENV OS_VERSION=${OS_VERSION} 8 | 9 | ## Set up environment variables so the tzdata install doesn't 10 | ## hang on asking for user input for configuration 11 | ARG DEBIAN_FRONTEND="noninteractive" 12 | ARG TZ="America/New_York" 13 | 14 | ## Install the deps and create the build directory 15 | RUN if [ ${OS_VERSION} -eq 10 ]; then \ 16 | apt-get update && \ 17 | apt-get install -y wget gpg && \ 18 | wget -nc https://apt.kitware.com/keys/kitware-archive-latest.asc && \ 19 | apt-key add kitware-archive-latest.asc && \ 20 | echo 'deb https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null; fi && \ 21 | apt-get update &&\ 22 | apt-get install -y --no-install-recommends git cmake build-essential \ 23 | gettext help2man libavcodec-dev libavformat-dev libswscale-dev \ 24 | ca-certificates file qtbase5-dev qtmultimedia5-dev && \ 25 | apt-get clean && \ 26 | mkdir /root/composer 27 | 28 | ## Copy in the build script to make things easy 29 | COPY build_composer.sh /root/composer/build_composer.sh 30 | 31 | WORKDIR /root/composer 32 | -------------------------------------------------------------------------------- /editor.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/application-exit.png 4 | icons/composer.png 5 | icons/document-new.png 6 | icons/document-save-as.png 7 | icons/document-save.png 8 | icons/edit-redo.png 9 | icons/edit-undo.png 10 | icons/edit-copy.png 11 | icons/edit-cut.png 12 | icons/edit-delete.png 13 | icons/edit-paste.png 14 | icons/edit-select-all.png 15 | icons/document-open.png 16 | icons/help-about.png 17 | icons/help-contents.png 18 | icons/help-hint.png 19 | icons/insert-object.png 20 | icons/insert-text.png 21 | icons/media-playback-pause.png 22 | icons/media-playback-start.png 23 | icons/media-playback-stop.png 24 | icons/preferences-other.png 25 | icons/zoom-in.png 26 | icons/zoom-original.png 27 | icons/zoom-out.png 28 | docs/helpindex.html 29 | docs/Authors.txt 30 | docs/License.txt 31 | 32 | 33 | -------------------------------------------------------------------------------- /cmake/FindAVUtil.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find FFMPEG libavutil 2 | # Once done, this will define 3 | # 4 | # AVUtil_FOUND - the library is available 5 | # AVUtil_INCLUDE_DIRS - the include directories 6 | # AVUtil_LIBRARIES - the libraries 7 | # AVUtil_INCLUDE - the file to #include (may be used in config.h) 8 | # 9 | # See documentation on how to write CMake scripts at 10 | # http://www.cmake.org/Wiki/CMake:How_To_Find_Libraries 11 | 12 | include(LibFindMacros) 13 | 14 | libfind_pkg_check_modules(AVUtil_PKGCONF libavutil) 15 | 16 | find_path(AVUtil_INCLUDE_DIR 17 | NAMES libavutil/avutil.h ffmpeg/avutil.h avutil.h 18 | HINTS ${AVUtil_PKGCONF_INCLUDE_DIRS} 19 | PATH_SUFFIXES ffmpeg 20 | ) 21 | 22 | if(AVUtil_INCLUDE_DIR) 23 | foreach(suffix libavutil/ ffmpeg/ "") 24 | if(NOT AVUtil_INCLUDE) 25 | if(EXISTS "${AVUtil_INCLUDE_DIR}/${suffix}avutil.h") 26 | set(AVUtil_INCLUDE "${suffix}avutil.h") 27 | endif(EXISTS "${AVUtil_INCLUDE_DIR}/${suffix}avutil.h") 28 | endif(NOT AVUtil_INCLUDE) 29 | endforeach(suffix) 30 | 31 | if(NOT AVUtil_INCLUDE) 32 | message(FATAL_ERROR "Found avutil.h include dir, but not the header file. Perhaps you need to clear CMake cache?") 33 | endif(NOT AVUtil_INCLUDE) 34 | endif(AVUtil_INCLUDE_DIR) 35 | 36 | find_library(AVUtil_LIBRARY 37 | NAMES libavutil.dll.a avutil 38 | HINTS ${AVUtil_PKGCONF_LIBRARY_DIRS} 39 | ) 40 | 41 | libfind_process(AVUtil) 42 | 43 | -------------------------------------------------------------------------------- /src/util.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | /** Implement C99 mathematical rounding (which C++ unfortunately currently lacks) **/ 7 | template T round(T val) { return int(val + (val >= 0 ? 0.5 : -0.5)); } 8 | 9 | /** Implement C99 remainder function (not precisely, but almost) **/ 10 | template T remainder(T val, T div) { return val - round(val/div) * div; } 11 | 12 | /** Limit val to range [min, max] **/ 13 | template T clamp(T val, T min = 0, T max = 1) { 14 | if (min > max) throw std::logic_error("min > max"); 15 | if (val < min) return min; 16 | if (val > max) return max; 17 | return val; 18 | } 19 | 20 | /** A convenient way for getting NaNs **/ 21 | static inline double getNaN() { return std::numeric_limits::quiet_NaN(); } 22 | 23 | /** A convenient way for getting infs **/ 24 | static inline double getInf() { return std::numeric_limits::infinity(); } 25 | 26 | static inline bool isPow2(unsigned int val) { 27 | if (val == 0) return false; 28 | if ((val & (val-1)) == 0) return true; // From Wikipedia: Power_of_two 29 | return false; 30 | } 31 | 32 | static inline unsigned int nextPow2(unsigned int val) { 33 | unsigned int ret = 1; 34 | while (ret < val) ret *= 2; 35 | return ret; 36 | } 37 | 38 | static inline unsigned int prevPow2(unsigned int val) { 39 | unsigned int ret = 1; 40 | while ((ret*2) < val) ret *= 2; 41 | return ret; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /cmake/FindSWScale.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find FFMPEG libswscale 2 | # Once done, this will define 3 | # 4 | # SWScale_FOUND - the library is available 5 | # SWScale_INCLUDE_DIRS - the include directories 6 | # SWScale_LIBRARIES - the libraries 7 | # SWScale_INCLUDE - the file to include (may be used in config.h) 8 | # 9 | # See documentation on how to write CMake scripts at 10 | # http://www.cmake.org/Wiki/CMake:How_To_Find_Libraries 11 | 12 | include(LibFindMacros) 13 | 14 | libfind_package(SWScale AVUtil) 15 | 16 | libfind_pkg_check_modules(SWScale_PKGCONF libswscale) 17 | 18 | find_path(SWScale_INCLUDE_DIR 19 | NAMES libswscale/swscale.h ffmpeg/swscale.h swscale.h 20 | HINTS ${SWScale_PKGCONF_INCLUDE_DIRS} 21 | PATH_SUFFIXES ffmpeg 22 | ) 23 | 24 | if(SWScale_INCLUDE_DIR) 25 | foreach(suffix libswscale/ ffmpeg/ "") 26 | if(NOT SWScale_INCLUDE) 27 | if(EXISTS "${SWScale_INCLUDE_DIR}/${suffix}swscale.h") 28 | set(SWScale_INCLUDE "${suffix}swscale.h") 29 | endif(EXISTS "${SWScale_INCLUDE_DIR}/${suffix}swscale.h") 30 | endif(NOT SWScale_INCLUDE) 31 | endforeach(suffix) 32 | 33 | if(NOT SWScale_INCLUDE) 34 | message(FATAL_ERROR "Found swscale.h include dir, but not the header file. Maybe you need to clear CMake cache?") 35 | endif(NOT SWScale_INCLUDE) 36 | endif(SWScale_INCLUDE_DIR) 37 | 38 | find_library(SWScale_LIBRARY 39 | NAMES libswscale.dll.a swscale 40 | HINTS ${SWScale_PKGCONF_LIBRARY_DIRS} 41 | ) 42 | 43 | libfind_process(SWScale) 44 | 45 | -------------------------------------------------------------------------------- /cmake/FindAVFormat.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find FFMPEG libavformat 2 | # Once done, this will define 3 | # 4 | # AVFormat_FOUND - the library is available 5 | # AVFormat_INCLUDE_DIRS - the include directories 6 | # AVFormat_LIBRARIES - the libraries 7 | # AVFormat_INCLUDE - the file to include (may be used in config.h) 8 | # 9 | # See documentation on how to write CMake scripts at 10 | # http://www.cmake.org/Wiki/CMake:How_To_Find_Libraries 11 | 12 | include(LibFindMacros) 13 | 14 | libfind_package(AVFormat AVCodec) 15 | 16 | libfind_pkg_check_modules(AVFormat_PKGCONF libavformat) 17 | 18 | find_path(AVFormat_INCLUDE_DIR 19 | NAMES libavformat/avformat.h ffmpeg/avformat.h avformat.h 20 | HINTS ${AVFormat_PKGCONF_INCLUDE_DIRS} 21 | PATH_SUFFIXES ffmpeg 22 | ) 23 | 24 | if(AVFormat_INCLUDE_DIR) 25 | foreach(suffix libavformat/ ffmpeg/ "") 26 | if(NOT AVFormat_INCLUDE) 27 | if(EXISTS "${AVFormat_INCLUDE_DIR}/${suffix}avformat.h") 28 | set(AVFormat_INCLUDE "${suffix}avformat.h") 29 | endif(EXISTS "${AVFormat_INCLUDE_DIR}/${suffix}avformat.h") 30 | endif(NOT AVFormat_INCLUDE) 31 | endforeach(suffix) 32 | 33 | if(NOT AVFormat_INCLUDE) 34 | message(FATAL_ERROR "Found avformat.h include dir, but not the header file. Perhaps you need to clear CMake cache?") 35 | endif(NOT AVFormat_INCLUDE) 36 | endif(AVFormat_INCLUDE_DIR) 37 | 38 | find_library(AVFormat_LIBRARY 39 | NAMES libavformat.dll.a avformat 40 | HINTS ${AVFormat_PKGCONF_LIBRARY_DIRS} 41 | ) 42 | 43 | libfind_process(AVFormat) 44 | 45 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | cmake_policy(VERSION 3.10) 3 | project("Composer" CXX C) 4 | set(PROJECT_VERSION "2.0.1") 5 | set(BUILD_SHARED_LIBS OFF) 6 | #FIXME: Changes in version number or project name must manually also be put to: 7 | # platform/mingw-cross-env/makeinstaller.py 8 | 9 | # Avoid source tree pollution 10 | if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) 11 | message(FATAL_ERROR "In-source builds are not permitted. Make a separate folder for building:\nmkdir build; cd build; cmake ..\nBefore that, remove the files already created:\nrm -rf CMakeCache.txt CMakeFiles") 12 | endif(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) 13 | 14 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/") 15 | set(EXECUTABLE_OUTPUT_PATH "${CMAKE_BINARY_DIR}") 16 | 17 | include(platform/packaging.cmake) 18 | 19 | # Add a sensible build type default and warning because empty means no optimization and no debug info. 20 | if(NOT CMAKE_BUILD_TYPE) 21 | message("WARNING: CMAKE_BUILD_TYPE is not defined!\n Defaulting to CMAKE_BUILD_TYPE=RelWithDebInfo. Use ccmake to set a proper value.") 22 | SET(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel." FORCE) 23 | endif(NOT CMAKE_BUILD_TYPE) 24 | 25 | # Find includes in corresponding build directories 26 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 27 | # Instruct CMake to run moc automatically when needed. 28 | set(CMAKE_AUTOMOC ON) 29 | 30 | # Sources 31 | add_subdirectory(src) 32 | -------------------------------------------------------------------------------- /cmake/FindAVCodec.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find FFMPEG libavcodec 2 | # Once done, this will define 3 | # 4 | # AVCodec_FOUND - the library is available 5 | # AVCodec_INCLUDE_DIRS - the include directories 6 | # AVCodec_LIBRARIES - the libraries 7 | # AVCodec_INCLUDE - the file to #include (may be used in config.h) 8 | # 9 | # See documentation on how to write CMake scripts at 10 | # http://www.cmake.org/Wiki/CMake:How_To_Find_Libraries 11 | 12 | include(LibFindMacros) 13 | 14 | libfind_package(AVCodec AVUtil) 15 | 16 | # TODO: pkg-config extra deps: libraw1394 theora vorbisenc 17 | 18 | libfind_pkg_check_modules(AVCodec_PKGCONF libavcodec) 19 | 20 | find_path(AVCodec_INCLUDE_DIR 21 | NAMES libavcodec/avcodec.h ffmpeg/avcodec.h avcodec.h 22 | HINTS ${AVCodec_PKGCONF_INCLUDE_DIRS} 23 | PATH_SUFFIXES ffmpeg 24 | ) 25 | 26 | if(AVCodec_INCLUDE_DIR) 27 | foreach(suffix libavcodec/ ffmpeg/ "") 28 | if(NOT AVCodec_INCLUDE) 29 | if(EXISTS "${AVCodec_INCLUDE_DIR}/${suffix}avcodec.h") 30 | set(AVCodec_INCLUDE "${suffix}avcodec.h" CACHE INTERNAL "") 31 | endif(EXISTS "${AVCodec_INCLUDE_DIR}/${suffix}avcodec.h") 32 | endif(NOT AVCodec_INCLUDE) 33 | endforeach(suffix) 34 | 35 | if(NOT AVCodec_INCLUDE) 36 | message(FATAL_ERROR "Found avcodec.h include dir, but not the header file. Perhaps you need to clear CMake cache?") 37 | endif(NOT AVCodec_INCLUDE) 38 | endif(AVCodec_INCLUDE_DIR) 39 | 40 | find_library(AVCodec_LIBRARY 41 | NAMES libavcodec.dll.a avcodec 42 | HINTS ${AVCodec_PKGCONF_LIBRARY_DIRS} 43 | ) 44 | 45 | libfind_process(AVCodec) 46 | 47 | -------------------------------------------------------------------------------- /cmake/FindSWResample.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find FFMPEG libswresample 2 | # Once done, this will define 3 | # 4 | # SWResample_FOUND - the library is available 5 | # SWResample_INCLUDE_DIRS - the include directories 6 | # SWResample_LIBRARIES - the libraries 7 | # SWResample_INCLUDE - the file to include (may be used in config.h) 8 | # 9 | # See documentation on how to write CMake scripts at 10 | # http://www.cmake.org/Wiki/CMake:How_To_Find_Libraries 11 | 12 | include(LibFindMacros) 13 | 14 | libfind_package(SWResample AVUtil) 15 | 16 | libfind_pkg_check_modules(SWResample_PKGCONF libswresample) 17 | 18 | find_path(SWResample_INCLUDE_DIR 19 | NAMES libswresample/swresample.h ffmpeg/swresample.h swresample.h 20 | HINTS ${SWResample_PKGCONF_INCLUDE_DIRS} 21 | PATH_SUFFIXES ffmpeg 22 | ) 23 | 24 | if(SWResample_INCLUDE_DIR) 25 | foreach(suffix libswresample/ ffmpeg/ "") 26 | if(NOT SWResample_INCLUDE) 27 | if(EXISTS "${SWResample_INCLUDE_DIR}/${suffix}swresample.h") 28 | set(SWResample_INCLUDE "${suffix}swresample.h") 29 | endif(EXISTS "${SWResample_INCLUDE_DIR}/${suffix}swresample.h") 30 | endif(NOT SWResample_INCLUDE) 31 | endforeach(suffix) 32 | 33 | if(NOT SWResample_INCLUDE) 34 | message(FATAL_ERROR "Found swresample.h include dir, but not the header file. Maybe you need to clear CMake cache?") 35 | endif(NOT SWResample_INCLUDE) 36 | endif(SWResample_INCLUDE_DIR) 37 | 38 | find_library(SWResample_LIBRARY 39 | NAMES libswresample.dll.a swresample 40 | HINTS ${SWResample_PKGCONF_LIBRARY_DIRS} 41 | ) 42 | 43 | libfind_process(SWResample) 44 | 45 | -------------------------------------------------------------------------------- /src/scrollbar.cc: -------------------------------------------------------------------------------- 1 | #include "scrollbar.hh" 2 | #include 3 | #include 4 | 5 | ScrollBar::ScrollBar(Qt::Orientation orientation, QWidget * parent) 6 | : QScrollBar(orientation, parent) { 7 | m_paintFunction = [](QImage&){}; 8 | } 9 | 10 | ScrollBar::ScrollBar(PaintFunction const& f, Qt::Orientation orientation, QWidget * parent) 11 | : QScrollBar(orientation, parent) { 12 | m_paintFunction = f; 13 | } 14 | 15 | void ScrollBar::update() { 16 | paint(); 17 | QScrollBar::update(); 18 | } 19 | 20 | void ScrollBar::paint() { 21 | m_paintFunction(m_background); 22 | } 23 | 24 | void ScrollBar::resizeEvent(QResizeEvent * event) { 25 | QScrollBar::resizeEvent(event); 26 | 27 | auto image = QImage(event->size(), QImage::Format_ARGB32); 28 | 29 | image.swap(m_background); 30 | 31 | paint(); 32 | } 33 | 34 | void ScrollBar::paintEvent(QPaintEvent* event) { 35 | //QScrollBar::paintEvent(event); 36 | 37 | const auto range = std::max(1, maximum() - minimum()); 38 | 39 | QPainter painter(this); 40 | 41 | painter.drawImage(0, 0, m_background); 42 | painter.setPen(QPen(Qt::gray,1)); 43 | 44 | if(orientation() == Qt::Horizontal) { 45 | const auto viewWidth = width(); 46 | const auto w = viewWidth * viewWidth / range; 47 | const auto x = value() * (width() - w) / range; 48 | 49 | painter.drawRect(x, y(), w - 1, height() - 1); 50 | } else { 51 | const auto viewHeight = height(); 52 | const auto h = viewHeight * viewHeight / range; 53 | const auto y = value() * (height() - h) / range; 54 | 55 | painter.drawRect(x(), y, width() - 1, h - 1); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cmake/Copyright.txt: -------------------------------------------------------------------------------- 1 | This license applies for the CMake modules in this directory unless otherwise specified. 2 | 3 | CMake - Cross Platform Makefile Generator 4 | Copyright 2000-2009 Kitware, Inc., Insight Software Consortium 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | * Neither the names of Kitware, Inc., the Insight Software Consortium, nor the names of their contributors may be used to endorse or promote products derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 16 | -------------------------------------------------------------------------------- /docs/FileFormats.txt: -------------------------------------------------------------------------------- 1 | Editor file format support 2 | ========================== 3 | 4 | This document describes to what extent various file formats are supported by the editor. 5 | 6 | 7 | Own project files 8 | ----------------- 9 | Native save/load format. Preserves operation history (undo buffer). 10 | 11 | 12 | SingStar XML 13 | ------------ 14 | Import and export. Round-trip is not perfect: All XML elements except MELODY, TRACK, SENTENCE and NOTE are ignored as are all attributes of SENTENCE-element. Exporter writes some useful comments. 15 | 16 | 17 | UltraStar TXT 18 | ------------- 19 | Import and export. All tags are not preserved. 20 | 21 | 22 | Frets on Fire MIDI/INI 23 | ---------------------- 24 | Vocal track import and export. 25 | 26 | 27 | Timecoded LRC / Soramimi lyrics 28 | ------------------------------- 29 | LRC is a popular karaoke format, which has many variations. Soramimi is an old karaoke software, which uses a similar format. Composer can import the timing data but the format has no way of expressing pitch. We attempt to support reading the timecodes in many different syntaxes, including per-word timing. For maximum compatibility, exporting LRC will use the simple format, which has one time per sentence - thus import-export is a lossy operation. Some "ID Tags" are supported. 30 | 31 | 32 | Plain text lyrics 33 | ----------------- 34 | Raw lyrics without any timing data can be read from a file or copied from clipboard. A note is created for each word and a line break indicates sentence end. 35 | 36 | 37 | Music files 38 | ----------- 39 | The pitch analysis can be performed to any file that FFmpeg can decode. Playback and metadata reading support depends on Phonon media library back-end. 40 | 41 | -------------------------------------------------------------------------------- /src/songwriter.hh: -------------------------------------------------------------------------------- 1 | #include "song.hh" 2 | #include 3 | #include 4 | 5 | struct SongWriter 6 | { 7 | SongWriter(const Song& s_, const QString& path_) 8 | : s(s_), path(path_) { QDir dir; dir.mkpath(path_); } 9 | const Song& s; 10 | QString path; 11 | }; 12 | 13 | struct SingStarXMLWriter: public SongWriter 14 | { 15 | SingStarXMLWriter(const Song& s_, const QString& path_) 16 | : SongWriter(s_, path_), tempo(s_.bpm > 0 ? s_.bpm : 180), res("Semiquaver") { writeXML(); } 17 | private: 18 | void writeXML(); 19 | int sec2dur(double sec) const; 20 | double dur2sec(int ts) const; 21 | int tempo; 22 | QString res; 23 | }; 24 | 25 | struct UltraStarTXTWriter: public SongWriter 26 | { 27 | UltraStarTXTWriter(const Song& s_, const QString& path_) 28 | : SongWriter(s_, path_), tempo(s_.bpm > 0 ? s_.bpm : 180) { writeTXT(); } 29 | private: 30 | void writeTXT() const; 31 | int sec2dur(double sec) const; 32 | int tempo; 33 | }; 34 | 35 | struct FoFMIDIWriter: public SongWriter 36 | { 37 | FoFMIDIWriter(const Song& s_, const QString& path_) 38 | : SongWriter(s_, path_) { writeINI(); writeMIDI(); } 39 | 40 | private: 41 | void writeINI() const; 42 | void writeMIDI() const; 43 | }; 44 | 45 | struct LRCWriter: public SongWriter 46 | { 47 | LRCWriter(const Song& s_, const QString& path_, bool enhanced = false) 48 | : SongWriter(s_, path_) { writeLRC(enhanced); } 49 | private: 50 | void writeLRC(bool enhancedLRC = false) const; 51 | QString sec2timestamp(double sec) const; 52 | QString EnhancedLRCsec2timestamp(double sec) const; 53 | }; 54 | 55 | struct SMMWriter: public SongWriter 56 | { 57 | SMMWriter(const Song& s_, const QString& path_) 58 | : SongWriter(s_,path_) { writeSMM(); } 59 | private: 60 | void writeSMM() const; 61 | QString sec2timestamp(double sec) const; 62 | }; 63 | -------------------------------------------------------------------------------- /platform/mingw-cross-env/mk/ffmpeg.mk: -------------------------------------------------------------------------------- 1 | # This file is part of mingw-cross-env. 2 | # See doc/index.html for further information. 3 | 4 | # ffmpeg 5 | PKG := ffmpeg 6 | $(PKG)_IGNORE := 7 | $(PKG)_VERSION := 0.6.1 8 | $(PKG)_CHECKSUM := 24ada1d35fc000980090e773101e101ca45f85e5 9 | $(PKG)_SUBDIR := $(PKG)-$($(PKG)_VERSION) 10 | $(PKG)_FILE := $(PKG)-$($(PKG)_VERSION).tar.bz2 11 | $(PKG)_WEBSITE := http://www.ffmpeg.org/ 12 | $(PKG)_URL := http://www.ffmpeg.org/releases/$($(PKG)_FILE) 13 | $(PKG)_URL_2 := http://launchpad.net/ffmpeg/main/$($(PKG)_VERSION)/+download/$($(PKG)_FILE) 14 | $(PKG)_DEPS := gcc bzip2 faad2 lame libvpx opencore-amr sdl speex theora vorbis x264 xvidcore zlib 15 | 16 | define $(PKG)_UPDATE 17 | wget -q -O- 'http://www.ffmpeg.org/download.html' | \ 18 | $(SED) -n 's,.*ffmpeg-\([0-9][^>]*\)\.tar.*,\1,p' | \ 19 | head -1 20 | endef 21 | 22 | define $(PKG)_BUILD 23 | cd '$(1)' && ./configure \ 24 | --cross-prefix='$(TARGET)'- \ 25 | --enable-cross-compile \ 26 | --arch=i686 \ 27 | --target-os=mingw32 \ 28 | --prefix='$(PREFIX)/$(TARGET)' \ 29 | --enable-shared \ 30 | --disable-static \ 31 | --disable-debug \ 32 | --disable-doc \ 33 | --enable-memalign-hack \ 34 | --enable-gpl \ 35 | --enable-version3 \ 36 | --disable-nonfree \ 37 | --enable-postproc \ 38 | --enable-libspeex \ 39 | --enable-libtheora \ 40 | --enable-libvorbis \ 41 | --enable-libmp3lame \ 42 | --enable-libxvid \ 43 | --enable-libfaad \ 44 | --disable-libfaac \ 45 | --enable-libopencore-amrnb \ 46 | --enable-libopencore-amrwb \ 47 | --enable-libx264 \ 48 | --enable-libvpx 49 | $(MAKE) -C '$(1)' -j '$(JOBS)' 50 | $(MAKE) -C '$(1)' -j 1 install 51 | endef 52 | -------------------------------------------------------------------------------- /src/gettingstarted.hh: -------------------------------------------------------------------------------- 1 | #include 2 | #include "ui_gettingstarted.h" 3 | #include "editorapp.hh" 4 | #include 5 | 6 | class GettingStartedDialog: public QDialog, private Ui::GettingStarted 7 | { 8 | Q_OBJECT 9 | public: 10 | GettingStartedDialog(QWidget* parent) 11 | : QDialog(parent), m_editorApp(qobject_cast(parent)) 12 | { 13 | if (!m_editorApp) throw std::runtime_error("Couldn't open help dialog."); 14 | setupUi(this); 15 | QSettings settings; 16 | chkShowOnStartup->setChecked(settings.value("showhelp", true).toBool()); 17 | } 18 | 19 | public slots: 20 | void on_cmdClose_clicked(bool) { close(); } 21 | 22 | void on_chkShowOnStartup_stateChanged(int state) { 23 | QSettings settings; 24 | settings.setValue("showhelp", state != Qt::Unchecked); 25 | } 26 | 27 | // Command link buttons 28 | 29 | void on_cmdMusicFile_clicked(bool) { 30 | m_editorApp->on_actionMusicFile_triggered(); 31 | } 32 | 33 | void on_cmdLyricsFromFile_clicked(bool) { 34 | m_editorApp->on_actionLyricsFromFile_triggered(); 35 | } 36 | 37 | void on_cmdLyricsFromClipboard_clicked(bool) { 38 | m_editorApp->on_actionLyricsFromClipboard_triggered(); 39 | } 40 | 41 | void on_cmdTimeLyrics_clicked(bool) { 42 | m_editorApp->highlightLabel("TIMING"); 43 | } 44 | 45 | void on_cmdFineTuneLyrics_clicked(bool) { 46 | m_editorApp->highlightLabel("TUNING"); 47 | } 48 | 49 | void on_cmdMetadata_clicked(bool) { 50 | m_editorApp->highlightLabel("SONG"); 51 | } 52 | 53 | void on_cmdExport_clicked(bool) { 54 | m_editorApp->showExportMenu(); 55 | } 56 | void on_cmdLRC_clicked(bool) { 57 | m_editorApp->on_actionLyricsFromLRCFile_triggered(); 58 | } 59 | 60 | protected: 61 | void closeEvent(QCloseEvent*) { 62 | QSettings settings; 63 | settings.setValue("showhelp", chkShowOnStartup->isChecked()); 64 | } 65 | 66 | private: 67 | EditorApp *m_editorApp; 68 | }; 69 | -------------------------------------------------------------------------------- /AppImageBuilder.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | AppDir: 4 | path: ./AppDir 5 | app_info: 6 | id: Performous Composer 7 | name: Composer 8 | icon: composer 9 | version: @@VERSION@@ 10 | exec: usr/bin/composer 11 | exec_args: $@ 12 | apt: 13 | arch: amd64 14 | allow_unauthenticated: true 15 | sources: 16 | - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal main restricted universe multiverse 17 | - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted universe multiverse 18 | include: 19 | ## All of these dependencies can be found by downloading the Composer 20 | ## package for Ubuntu 20.04 and doing a dpkg -I on it 21 | - libavcodec58 22 | - libavformat58 23 | - libavutil56 24 | - libc6 25 | - libgcc-s1 26 | - libqt5core5a 27 | - libqt5gui5 28 | - libqt5multimedia5 29 | - libqt5widgets5 30 | - libqt5xml5 31 | - libstdc++6 32 | - libswresample3 33 | files: 34 | exclude: 35 | - usr/share/man 36 | - usr/share/doc/*/README.* 37 | - usr/share/doc/*/changelog.* 38 | - usr/share/doc/*/NEWS.* 39 | - usr/share/doc/*/TODO.* 40 | after_bundle: | 41 | ## In Fedora and Gentoo (probably others too), libnsl has been deprecated 42 | ## from glibc and is available in another package. It still exists as part 43 | ## of the Debian lineage, and must be copied into the project root so it 44 | ## can run on Distros where it is not available by default. 45 | ## Hopefully this can be removed some day. 46 | cp $TARGET_APPDIR/runtime/compat/lib/x86_64-linux-gnu/libnsl.so.1 $TARGET_APPDIR/ 47 | ## This needs to be copied to make Arch work 48 | cp $TARGET_APPDIR/runtime/compat/lib/x86_64-linux-gnu/libcrypt.so.1 $TARGET_APPDIR/ 49 | AppImage: 50 | arch: x86_64 51 | update-information: None 52 | sign-key: None 53 | -------------------------------------------------------------------------------- /src/songwriter-lrc.cc: -------------------------------------------------------------------------------- 1 | #include "songwriter.hh" 2 | #include "config.hh" 3 | #include "util.hh" 4 | #include 5 | #include 6 | 7 | 8 | void LRCWriter::writeLRC(bool enhancedLRC) const { 9 | QFile f(path + "/song.lrc"); 10 | if (!f.open(QFile::WriteOnly | QFile::Truncate)) 11 | throw std::runtime_error("Couldn't open target file"); 12 | 13 | QTextStream out(&f); 14 | out.setCodec("UTF-8"); 15 | 16 | // Meta fields 17 | out << "[ti:" << (s.title.isEmpty() ? "Unknown" : s.title) << "]\n"; 18 | out << "[ar:" << (s.artist.isEmpty() ? "Unknown" : s.artist) << "]\n"; 19 | out << "[re:" << PACKAGE << "]\n"; 20 | out << "[ve:" << VERSION << "]\n"; 21 | if (!s.creator.isEmpty()) out << "[by:" << s.creator << "]\n"; 22 | 23 | // Loop through the notes 24 | const Notes& notes = s.getVocalTrack().notes; 25 | for (int i = 0; i < notes.size(); ++i) { 26 | const Note& n = notes[i]; 27 | if (n.type == Note::SLEEP) continue; 28 | 29 | // Put timestamp before new phrases 30 | if (i == 0 || n.lineBreak) 31 | out << '\n' << sec2timestamp(n.begin); 32 | if(enhancedLRC) //if using "enhanced LRC use <> timestamps! 33 | out << n.syllable << EnhancedLRCsec2timestamp(n.begin); 34 | else // Output the lyrics 35 | out << n.syllable; 36 | } 37 | out << '\n'; 38 | } 39 | 40 | 41 | QString LRCWriter::sec2timestamp(double sec) const { 42 | double modsec = std::fmod(sec, 60.0); 43 | return QString("[%1:%2.%3]") 44 | .arg(int(sec/60), 2, 10, QChar('0')) 45 | .arg(int(modsec), 2, 10, QChar('0')) 46 | .arg(int(100 * (modsec - int(modsec))), 2, 10, QChar('0')); 47 | } 48 | 49 | QString LRCWriter::EnhancedLRCsec2timestamp(double sec) const { 50 | double modsec = std::fmod(sec, 60.0); 51 | return QString("<%1:%2.%3>") 52 | .arg(int(sec/60), 2, 10, QChar('0')) 53 | .arg(int(modsec), 2, 10, QChar('0')) 54 | .arg(int(100 * (modsec - int(modsec))), 2, 10, QChar('0')); 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/pitchvis.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "notes.hh" 4 | #include "util.hh" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | struct PitchFragment { 15 | float time, note, level; // seconds, MIDI note, dB 16 | PitchFragment(float time, float note, float level): time(time), note(note), level(level) {} 17 | }; 18 | 19 | struct PitchPath { 20 | typedef std::vector Fragments; 21 | Fragments fragments; 22 | unsigned channel; 23 | PitchPath(unsigned channel): channel(channel) {} 24 | }; 25 | 26 | class NoteGraphWidget; 27 | 28 | class PitchVis: public QThread 29 | { 30 | Q_OBJECT 31 | public: 32 | typedef std::vector Paths; 33 | QMutex mutex; 34 | 35 | PitchVis(QString const& filename, QWidget *parent = NULL, int visId = 0); 36 | ~PitchVis() { stop(); wait(); } 37 | 38 | void stop(); 39 | void cancel(); 40 | void paint(int x1, int y1, int x2, int y2); 41 | bool newDataAvailable() const { return moreAvailable; } 42 | double getProgress() const { return position / duration; } 43 | double getDuration() const { return duration; } 44 | int guessNote(double begin, double end, int initial); 45 | 46 | signals: 47 | void renderedImage(const QImage &image, const QPoint &position, int visId); 48 | 49 | protected: 50 | void run(); // Thread runs here 51 | 52 | private: 53 | void renderer(); 54 | Paths const& getPaths() { moreAvailable = false; return paths; } 55 | 56 | MusicalScale scale; 57 | QString fileName; 58 | Paths paths; 59 | double position; ///< Position while analyzing 60 | double duration; ///< Song duration (or estimation while analyzing) 61 | bool moreAvailable; 62 | bool quit; ///< Quit at the frst chance 63 | bool cancelled; ///< Cancel analyzing, but use what was done so far 64 | bool restart; ///< Should we start the rendering again? 65 | QWaitCondition condition; 66 | int m_x1, m_y1, m_x2, m_y2; 67 | int m_visId; 68 | }; 69 | 70 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | cmake_policy(VERSION 3.10) 3 | 4 | set(EXENAME ${CMAKE_PROJECT_NAME}) 5 | set(CMAKE_AUTOMOC FALSE) 6 | if(UNIX) 7 | # On UNIX, binary name is lowercase with no spaces 8 | string(TOLOWER ${EXENAME} EXENAME) 9 | string(REPLACE " " "-" EXENAME ${EXENAME}) 10 | endif() 11 | 12 | # Headers that need MOC need to be defined separately 13 | file(GLOB MOC_HEADER_FILES editorapp.hh notelabel.hh notegraphwidget.hh textcodecselector.hh gettingstarted.hh pitchvis.hh synth.hh) 14 | 15 | file(GLOB SOURCE_FILES "*.cc") 16 | file(GLOB HEADER_FILES "*.hh") 17 | file(GLOB RESOURCE_FILES "../*.qrc") 18 | file(GLOB UI_FILES "../ui/*.ui") 19 | 20 | # Find all the libs that don't require extra parameters 21 | 22 | # Final binary 23 | add_executable(${EXENAME}) 24 | 25 | foreach(lib AVFormat SWResample SWScale Qt5Core Qt5Widgets Qt5Gui Qt5Xml Qt5Multimedia) 26 | find_package(${lib} REQUIRED) 27 | message(STATUS "${lib} includes: ${${lib}_INCLUDE_DIRS}") 28 | target_include_directories(${EXENAME} SYSTEM PRIVATE ${${lib}_INCLUDE_DIRS}) 29 | target_link_libraries(${EXENAME} PRIVATE ${${lib}_LIBRARIES}) 30 | endforeach(lib) 31 | 32 | # Qt pre-processors 33 | QT5_ADD_RESOURCES(RESOURCE_SOURCES ${RESOURCE_FILES}) 34 | QT5_WRAP_UI(UI_SOURCES ${UI_FILES} ) 35 | QT5_WRAP_CPP(MOC_SOURCES ${MOC_HEADER_FILES}) 36 | 37 | target_sources(${EXENAME} PRIVATE ${HEADER_FILES} ${SOURCE_FILES} ${MOC_SOURCES} ${RESOURCE_SOURCES} ${UI_SOURCES}) 38 | 39 | 40 | # Generate config.hh 41 | configure_file(config.cmake.hh "${CMAKE_BINARY_DIR}/src/config.hh" @ONLY) 42 | 43 | target_include_directories(${EXENAME} PRIVATE ${CMAKE_BINARY_DIR}/src) 44 | target_include_directories(${EXENAME} PRIVATE ${CMAKE_SOURCE_DIR}/src) 45 | 46 | # We don't currently have any assets, so on Windows, we just install to the root installation folder 47 | if(UNIX) 48 | install(TARGETS ${EXENAME} DESTINATION bin) 49 | install(FILES "../platform/composer.desktop" DESTINATION "share/applications/") 50 | install(FILES "../icons/composer.png" DESTINATION "share/pixmaps") 51 | else() 52 | install(TARGETS ${EXENAME} DESTINATION .) 53 | endif() 54 | -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "config.hh" 5 | #include "editorapp.hh" 6 | 7 | #ifdef STATIC_PLUGINS 8 | #include 9 | Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin) 10 | #endif 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | Q_INIT_RESOURCE(editor); 15 | 16 | QApplication app(argc, argv); 17 | // These values are used by e.g. Phonon and QSettings 18 | app.setApplicationName(PACKAGE); 19 | app.setApplicationVersion(VERSION); 20 | app.setOrganizationName("Performous Team"); 21 | app.setOrganizationDomain("performous.org"); 22 | 23 | // Command line parsing 24 | // Unfortunately Qt doesn't include proper interface for this, 25 | // even though it does it internally (and handles some args). 26 | // This here is not very elegant, but didn't want to introduce 27 | // additional dependency for a couple of very simple options. 28 | QStringList args = QApplication::arguments(); 29 | QString openpath = ""; 30 | for (int i = 1; i < args.size(); ++i) { // FIXME: On Windows arg0 might or might not be the program name 31 | if (args[i] == "--version" || args[i] == "-v") { 32 | std::cout << VERSION << std::endl; 33 | exit(EXIT_SUCCESS); 34 | } 35 | else if (args[i] == "--help" || args[i] == "-h") { 36 | std::cout << PACKAGE << " " << VERSION << std::endl << std::endl 37 | << "-h [ --help ] you are viewing it" << std::endl 38 | << "-v [ --version ] display version number" << std::endl 39 | << "argument without a switch is interpreted as a song file to open" << std::endl 40 | ; 41 | exit(EXIT_SUCCESS); 42 | } 43 | else if (!args[i].startsWith("-")) openpath = args[i]; // No switch 44 | else { 45 | std::cout << "Unknown option: " << args[i].toStdString() << std::endl; 46 | exit(EXIT_FAILURE); 47 | } 48 | } 49 | 50 | // Localization 51 | QString locale = QLocale::system().name(); 52 | QTranslator translator; 53 | translator.load(locale); 54 | app.installTranslator(&translator); 55 | 56 | EditorApp window; 57 | window.show(); 58 | 59 | if (!openpath.isEmpty()) window.openFile(openpath); // Load song if given in command line 60 | 61 | return app.exec(); 62 | } 63 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## How to Write a Good Issue 4 | 5 | Please keep in mind that the GitHub issue tracker is not intended as a general support forum, but for reporting bugs and feature requests. 6 | For end-user related support questions, please refer to one of the following: 7 | 8 | - Discord Channel General: https://discord.gg/NS3m3ad 9 | 10 | ### Title 11 | 12 | The title must be short and descriptive. (~60 characters) 13 | 14 | ### Description 15 | 16 | - Respect the issue template as much as possible. [template](.github/ISSUE_TEMPLATE.md) 17 | - Explain the conditions which led you to write this issue: the context. 18 | - The context should lead to something, an idea or a problem that you’re facing. 19 | - Remain clear and concise. 20 | - Format your messages to help the reader focus on what matters and understand the structure of your message, use [Markdown syntax](https://help.github.com/articles/github-flavored-markdown) 21 | 22 | 23 | ## How to Write a Good Pull Request 24 | 25 | ### Title 26 | 27 | The title must be short and descriptive. (~60 characters) 28 | 29 | ### Description 30 | 31 | - Respect the pull request template as much as possible. [template](.github/PULL_REQUEST_TEMPLATE.md) 32 | - Explain the conditions which led you to write this PR: the context. 33 | - The context should lead to something, an idea or a problem that you’re facing. 34 | - Remain clear and concise. 35 | - Format your messages to help the reader focus on what matters and understand the structure of your message, use [Markdown syntax](https://help.github.com/articles/github-flavored-markdown) 36 | 37 | ### Content 38 | 39 | - Make it small. 40 | - Do only one thing. 41 | - Write useful descriptions and titles. 42 | - Avoid re-formatting. 43 | - Make sure the code builds. 44 | - Make sure all tests pass. 45 | - Add tests. 46 | - Address review comments in terms of additional commits. 47 | - Do not amend/squash existing ones unless the PR is trivial. 48 | - If a PR involves changes to third-party dependencies, the commits pertaining to the vendor folder and the manifest/lock file(s) should be committed separated. 49 | 50 | 51 | Read [10 tips for better pull requests](http://blog.ploeh.dk/2015/01/15/10-tips-for-better-pull-requests/). 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature or enhancement for Composer. 3 | labels: ["Feature request"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | #### ADVISORY 9 | 10 | "Please post all details in **English**." 11 | 12 | #### Prerequisites before submitting a feature request! 13 | - Read the issue reporting section in the **[contributing guidelines](https://github.com/performous/composer/blob/master/.github/CONTRIBUTING.md#how-to-write-a-good-issue)**, to know how to submit a feature request with the required information. 14 | - Verify that the feature being requested is not available in the **[latest official Performous version](https://github.com/performous/composer/releases/latest).** 15 | - (Optional, but recommended) Verify that feature being requested is not available in the latest **[CI builds](https://github.com/performous/composer/actions?query=event%3Apush+is%3Acompleted+branch%3Amaster+workflow%3A%22Build+and+Release+Composer%22++)**. 16 | - Perform a **[search of the issue tracker (including closed ones)](https://github.com/performous/composer/issues)** to avoid posting a duplicate. 17 | - Make sure this is not a support request or question, both of which are better suited for either the **[discussions section](https://github.com/performous/composer/discussions)** or **[Discord - User Support channel](https://discord.gg/NS3m3ad)**. 18 | - Verify that the **[wiki](https://github.com/performous/composer/wiki)** did not contain a suitable solution either. 19 | 20 | - type: textarea 21 | attributes: 22 | label: Suggestion 23 | validations: 24 | required: false 25 | 26 | - type: textarea 27 | attributes: 28 | label: Use case 29 | description: Provide a valid usecase in which this new feature can be used. This will help us giving some context to the problem we're trying to solve. 30 | validations: 31 | required: false 32 | 33 | - type: textarea 34 | attributes: 35 | label: Extra info/examples/attachments 36 | description: Add screenshots etc. (Anything that will give us more context about what is being requested!) 37 | validations: 38 | required: false -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "configurePresets": [ 4 | { 5 | "name": "windows-base", 6 | "description": "Target Windows with the Visual Studio development environment.", 7 | "hidden": true, 8 | "generator": "Ninja", 9 | "binaryDir": "${sourceDir}/build/${presetName}", 10 | "cacheVariables": { 11 | "CMAKE_C_COMPILER": "cl.exe", 12 | "CMAKE_CXX_COMPILER": "cl.exe", 13 | "BUILD_SHARED_LIBS": "ON", 14 | "ENABLE_WEBSERVER": "OFF", 15 | "SELF_BUILT_AUBIO": "ALWAYS", 16 | "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}-install", 17 | "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", 18 | "COMPOSER_VERSION": "$env{COMPOSER_VERSION}" 19 | } 20 | }, 21 | { 22 | "name": "x64-debug", 23 | "displayName": "x64 Debug", 24 | "description": "Target Windows Debug (64-bit).", 25 | "inherits": "windows-base", 26 | "cacheVariables": { 27 | "CMAKE_BUILD_TYPE": "Debug" 28 | } 29 | }, 30 | { 31 | "name": "x64-release", 32 | "displayName": "x64 Release", 33 | "description": "Target Windows Release (64-bit).", 34 | "inherits": "windows-base", 35 | "cacheVariables": { 36 | "CMAKE_BUILD_TYPE": "Release" 37 | } 38 | }, 39 | { 40 | "name": "x64-debinfo", 41 | "displayName": "x64 Release with Debug info", 42 | "description": "Target Windows Release with Debug info (64-bit).", 43 | "inherits": "x64-release", 44 | "cacheVariables": { 45 | "CMAKE_BUILD_TYPE": "RelWithDebInfo" 46 | } 47 | } 48 | ], 49 | "buildPresets": [ 50 | { 51 | "name": "x64-debug", 52 | "configurePreset": "x64-debug", 53 | "displayName": "Build x64-debug.", 54 | "description": "Build x64-debug." 55 | }, 56 | { 57 | "name": "x64-release", 58 | "configurePreset": "x64-release", 59 | "displayName": "Build x64-release.", 60 | "description": "Build x64-release." 61 | }, 62 | { 63 | "name": "x64-debinfo", 64 | "configurePreset": "x64-debinfo", 65 | "displayName": "Build x64-debinfo.", 66 | "description": "Build x64-debinfo." 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | This folder contains Dockerfiles which will install all dependencies needed to build [Composer](https://github.com/performous/composer/wiki/Building-and-installing-from-source). 2 | 3 | These containers are to be used as `base images` to provide higher-level builds and packages and to produce artifacts for downstream consumption. These containers **do not** provide a running version of `Composer` or contain the project source in any usable form. 4 | 5 | These containers are built automatically during our CI/CD workflow, and are used to support Linux Distros that are no available by default from Github Actions. 6 | 7 | ## Building containers 8 | The build is a pretty standard `docker build`, just make sure you explicitly call out a `Dockerfile` with `-f Dockerfile.` and supply the correct distro version as a `build-arg`: 9 | ```sh 10 | docker build -t composer-docker-build:ubuntu20.04 -f Dockerfile.ubuntu --build-arg OS_VERSION=20.04 . 11 | ``` 12 | 13 | Currently supported distros are: 14 | - Ubuntu (20.04, 22.04) 15 | - Fedora (34, 35, 36) 16 | - Debian (10, 11) 17 | 18 | ## Running the containers 19 | Once the `base-image` has been built, the container can be run interactively to build `Composer`: 20 | ```sh 21 | docker run -it composer-docker-build:ubuntu20.04 22 | ``` 23 | 24 | From there, you can [follow the build instructions](https://github.com/performous/composer/wiki/Building-and-installing-from-source#downloading-and-installing-the-sources) to build composer. 25 | 26 | 27 | `build_composer.sh` is included in the containers for testing builds and creating OS packages. 28 | ``` 29 | Usage: ./build_composer.sh -a (build with all build systems) 30 | 31 | Optional Arguments: 32 | -b : Build the specified git branch, tag, or sha 33 | -p : Build the specified Github Pull Request number 34 | -g : Generate Packages 35 | -r : Git repository to pull from 36 | -R : Perform a 'Release' Cmake Build (Default is 'RelWithDebInfo') 37 | -h : Show this help message 38 | ``` 39 | 40 | To build a pull request using just cmake: 41 | ``` 42 | docker run composer-docker-build:ubuntu20.04 ./build_composer.sh -c -p 626 43 | ``` 44 | 45 | One-Liner to generate packages and copy them to `/tmp` on the running system: 46 | ``` 47 | mkdir /tmp/composer-packages && docker run --rm --mount type=bind,source=/tmp/composer-packages,target=/composer/packages composer-docker-build:ubuntu20.04 /bin/bash -c './build_composer.sh -c -R -g && cp composer/build/*.deb /composer/packages' 48 | ``` 49 | -------------------------------------------------------------------------------- /src/songwriter-txt.cc: -------------------------------------------------------------------------------- 1 | #include "songwriter.hh" 2 | #include "config.hh" 3 | #include "util.hh" 4 | #include 5 | 6 | 7 | void UltraStarTXTWriter::writeTXT() const { 8 | QFile f(path + "/notes.txt"); 9 | if (!f.open(QFile::WriteOnly | QFile::Truncate)) 10 | throw std::runtime_error("Couldn't open target file"); 11 | 12 | // Figure out song filename 13 | QString mp3 = "NO_SONG"; 14 | if (!s.music["EDITOR"].isEmpty()) { 15 | QFileInfo finfo(s.music["EDITOR"]); 16 | mp3 = finfo.fileName(); 17 | } 18 | 19 | QTextStream out(&f); 20 | out.setCodec("UTF-8"); 21 | 22 | // Required fields 23 | out << "#TITLE:" << (s.title.isEmpty() ? "Unknown" : s.title) << '\n'; 24 | out << "#ARTIST:" << (s.artist.isEmpty() ? "Unknown" : s.artist) << '\n'; 25 | out << "#MP3:" << mp3 << '\n'; 26 | out << "#BPM:" << tempo << '\n'; 27 | out << "#GAP:" << "0" << '\n'; // Time to first lyric in milliseconds 28 | 29 | // Additional fields 30 | out << "#CREATOR:" << (s.creator.isEmpty() ? PACKAGE : s.creator) << '\n'; 31 | if (!s.genre.isEmpty()) out << "#GENRE:" << s.genre << '\n'; 32 | if (!s.year.isEmpty()) out << "#YEAR:" << s.year << '\n'; 33 | if (!s.language.isEmpty()) out << "#LANGUAGE:" << s.language << '\n'; 34 | if (!s.edition.isEmpty()) out << "#EDITION:" << s.edition << '\n'; 35 | if (!s.cover.isEmpty()) out << "#COVER:" << s.cover << '\n'; 36 | if (!s.background.isEmpty()) out << "#BACKGROUND:" << s.background << '\n'; 37 | if (!s.video.isEmpty()) out << "#VIDEO:" << s.video << '\n'; 38 | 39 | // The following are not useful, at least for now 40 | //if (s.videoGap != 0) out << "#VIDEOGAP:" << s.videoGap << '\n'; 41 | //if (start != 0) out << "#START:" << s.start << '\n'; 42 | //out << "#RELATIVE:" << "no" << '\n'; 43 | //out << "#PREVIEWSTART:" << s.preview_start << '\n'; 44 | //if (!s.music["vocals"].isEmpty()) out << "#VOCALS:" << s.music["vocals"] << '\n'; // FIXME: remove full path 45 | 46 | // Loop through the notes 47 | const Notes& notes = s.getVocalTrack().notes; 48 | for (int i = 0; i < notes.size(); ++i) { 49 | const Note& n = notes[i]; 50 | if (n.type == Note::SLEEP) continue; 51 | 52 | // Put sleeps between phrases 53 | if (i > 0 && n.lineBreak) { 54 | double ts = 0.5 * (notes[i-1].end + n.begin); 55 | out << "- " << sec2dur(ts) << '\n'; 56 | } 57 | 58 | // Output the note 59 | out << (char)n.type << ' '<< sec2dur(n.begin) << ' ' << sec2dur(n.length()) << ' ' << n.note << ' ' << n.syllable << '\n'; 60 | } 61 | 62 | out << "E"; // End indicator 63 | } 64 | 65 | 66 | int UltraStarTXTWriter::sec2dur(double sec) const { 67 | return round(tempo / 60.0 * sec * 4); 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/build_functions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ########################################################## 3 | ## 4 | ## These functions are meant to be pulled into stages 5 | ## of the various workflows that require common tasks, 6 | ## such as finding the package that was just created. 7 | ## This is meant to reduce the size of the workflow 8 | ## files, and to utilize a single set of shared code 9 | ## among all the workflows, since copying these 10 | ## everywhere is error-prone and tedious. 11 | ## 12 | ## For simplicity, these functions should work on all 13 | ## UNIX(like) platforms without any special treatment 14 | ## needed to detect OS type, lineage, etc. 15 | ## 16 | ########################################################## 17 | 18 | ## This function finds the package names produced by the 19 | ## build, renames the artifact as appropriate, and sets 20 | ## the outputs that will be consumed by later actions. 21 | ## The first argument is a path to the working drectory, 22 | ## usually "$(pwd)". The second argument is a 23 | ## bash-compatible regex to search for the package. 24 | ## The third argument is the package version as reported 25 | ## by ${{ inputs.package_complete_version }}. 26 | ## The fourth argument is the OS name, and the fifth 27 | ## argument is the OS version, both are optional. 28 | function package_name () { 29 | WORK_DIR=${1} 30 | PACKAGE_REGEX=${2} 31 | PACKAGE_OFFICIAL_VERSION=${3} 32 | PACKAGE_OS=${4} 33 | PACKAGE_OS_VERSION=${5} 34 | 35 | if [ ${PACKAGE_OS} ]; then 36 | PACKAGE_OS_NAME="-${PACKAGE_OS}" 37 | fi 38 | if [ ${PACKAGE_OS_VERSION} ]; then 39 | PACKAGE_OS_VERSION_NAME="_${PACKAGE_OS_VERSION}" 40 | fi 41 | PACKAGE_PATH=$(ls ${WORK_DIR}/${PACKAGE_REGEX}) 42 | PACKAGE_NAME=$(basename ${PACKAGE_PATH}) 43 | PACKAGE_SUFFIX=$(echo ${PACKAGE_NAME} | rev | cut -d'.' -f1 | rev) 44 | NEW_PACKAGE_NAME="Composer-${PACKAGE_OFFICIAL_VERSION}${PACKAGE_OS_NAME}${PACKAGE_OS_VERSION_NAME}.${PACKAGE_SUFFIX}" 45 | NEW_PACKAGE_PATH="/tmp/${NEW_PACKAGE_NAME}" 46 | MASTER_NEW_PACKAGE_NAME="Composer-latest${PACKAGE_OS_NAME}${PACKAGE_OS_VERSION_NAME}.${PACKAGE_SUFFIX}" 47 | MASTER_NEW_PACKAGE_PATH="/tmp/${MASTER_NEW_PACKAGE_NAME}" 48 | cp ${PACKAGE_PATH} ${MASTER_NEW_PACKAGE_PATH} 49 | cp ${PACKAGE_PATH} ${NEW_PACKAGE_PATH} 50 | ARTIFACT_NAME=$(basename ${NEW_PACKAGE_NAME}) 51 | MASTER_ARTIFACT_NAME=$(basename ${MASTER_NEW_PACKAGE_NAME}) 52 | echo "ARTIFACT_PATH=${NEW_PACKAGE_PATH}" >> ${GITHUB_ENV} 53 | echo "ARTIFACT_NAME=${NEW_PACKAGE_NAME}" >> ${GITHUB_ENV} 54 | echo "MASTER_ARTIFACT_PATH=${MASTER_NEW_PACKAGE_PATH}" >> ${GITHUB_ENV} 55 | echo "MASTER_ARTIFACT_NAME=${MASTER_NEW_PACKAGE_NAME}" >> ${GITHUB_ENV} 56 | } 57 | -------------------------------------------------------------------------------- /docker/build_composer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | ## Pull in /etc/os-release so we can see what we're running on 3 | . /etc/os-release 4 | 5 | ## Default Vars 6 | GIT_REPOSITORY='https://github.com/performous/composer.git' 7 | 8 | ## Function to print the help message 9 | usage() { 10 | set +x 11 | echo "" 12 | echo "Usage: ${0}" 13 | echo "" 14 | echo "Optional Arguments:" 15 | echo " -b : Build the specified git branch, tag, or sha" 16 | echo " -D : Disable cloning the repo from git and build in the specified directory" 17 | echo " -E <'Extra Cmake Args'>: A quoted list of extra arguments to pass directly to cmake" 18 | echo " -g : Generate Packages" 19 | echo " -p : Build the specified Github Pull Request number" 20 | echo " -r : Git repository to pull from" 21 | echo " -R : Perform a 'Release' Cmake Build (Default is 'RelWithDebInfo')" 22 | echo " -h : Show this help message" 23 | exit 1 24 | } 25 | 26 | ## Set up getopts 27 | while getopts "b:D:E:gp:r:Rh" OPTION; do 28 | case ${OPTION} in 29 | "b") 30 | GIT_BRANCH=${OPTARG};; 31 | "D") 32 | BUILD_DIRECTORY=${OPTARG};; 33 | "E") 34 | EXTRA_CMAKE_ARGS=${OPTARG};; 35 | "g") 36 | GENERATE_PACKAGES=true;; 37 | "p") 38 | PULL_REQUEST=${OPTARG};; 39 | "r") 40 | GIT_REPOSITORY=${OPTARG};; 41 | "R") 42 | RELEASE_BUILD=true;; 43 | "h") 44 | HELP=true;; 45 | esac 46 | done 47 | 48 | if [ ${HELP} ]; then 49 | usage 50 | fi 51 | 52 | ## All the git stuff 53 | if [ -z ${BUILD_DIRECTORY} ]; then 54 | git clone ${GIT_REPOSITORY} performous_composer 55 | cd performous_composer 56 | if [ ${PULL_REQUEST} ]; then 57 | git fetch origin pull/${PULL_REQUEST}/head:pr 58 | git checkout pr 59 | elif [ ${GIT_BRANCH} ]; then 60 | git checkout ${GIT_BRANCH} 61 | fi 62 | git submodule update --init --recursive 63 | else 64 | cd ${BUILD_DIRECTORY} 65 | fi 66 | 67 | ## Set up some special cmake flags for fedora 68 | if [ "${ID}" == "fedora" ]; then 69 | EXTRA_CMAKE_ARGS="${EXTRA_CMAKE_ARGS} -DUSE_BOOST_REGEX=1" 70 | fi 71 | 72 | if [ "${RELEASE_BUILD}" ]; then 73 | EXTRA_CMAKE_ARGS="${EXTRA_CMAKE_ARGS} -DCMAKE_BUILD_TYPE=Release" 74 | fi 75 | 76 | ## Figure out what type of packages we need to generate 77 | case ${ID} in 78 | 'fedora') 79 | PACKAGE_TYPE='RPM';; 80 | 'ubuntu'|'debian') 81 | PACKAGE_TYPE='DEB';; 82 | *) 83 | PACKAGE_TYPE='TGZ';; 84 | esac 85 | 86 | ## Build with cmake 87 | mkdir build 88 | cd build 89 | cmake ${EXTRA_CMAKE_ARGS} -DENABLE_WEBSERVER=ON -DCMAKE_VERBOSE_MAKEFILE=1 -DENABLE_WEBCAM=ON .. 90 | CPU_CORES=$(nproc --all) 91 | make -j${CPU_CORES} 92 | if [ ${GENERATE_PACKAGES} ]; then 93 | cpack -G ${PACKAGE_TYPE} 94 | fi 95 | cd .. 96 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report to help improve Composer user experience. 3 | labels: ["Bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | #### ADVISORY 9 | "We do not support any versions older than the current release series" 10 | 11 | "Please post all details in **English**." 12 | 13 | #### Prerequisites before submitting an issue! 14 | - Read the issue reporting section in the **[contributing guidelines](https://github.com/performous/composer/blob/master/.github/CONTRIBUTING.md#how-to-write-a-good-issue)**, to know how to submit a good bug report with the required information. 15 | - Verify that the issue is not fixed and is reproducible in the **[latest official Performous version](https://github.com/performous/composer/releases/latest).** 16 | - (Optional, but recommended) Verify that the issue is not fixed and is reproducible in the latest **[CI builds](https://github.com/performous/composer/actions?query=event%3Apush+is%3Acompleted+branch%3Amaster+workflow%3A%22Build+and+Release+Composer%22++)**. 17 | - Perform a **[search of the issue tracker (including closed ones)](https://github.com/performous/composer/issues)** to avoid posting a duplicate. 18 | - Make sure this is not a support request or question, both of which are better suited for either the **[discussions section](https://github.com/performous/composer/discussions)** or **[Discord - User Support channel](https://discord.gg/NS3m3ad)**. 19 | - Verify that the **[wiki](https://github.com/performous/composer/wiki)** did not contain a suitable solution either. 20 | 21 | - type: textarea 22 | attributes: 23 | label: Composer & operating system versions 24 | description: | 25 | Composer version can be found with: composer --version 26 | 27 | Example of preferred formatting: 28 | Composer: 1.2.0 29 | Operating system: Windows 10 Pro 21H1/2009 x64 30 | placeholder: | 31 | Composer: 32 | Operating system: 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | attributes: 38 | label: What is the problem? 39 | description: Please give a clear and concise description of the problem. 40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | attributes: 45 | label: Steps to reproduce 46 | description: Please provide reliable steps to reproduce the problem. 47 | placeholder: | 48 | 1. First step 49 | 2. Second step 50 | 3. and so on... 51 | validations: 52 | required: false 53 | 54 | - type: textarea 55 | attributes: 56 | label: Additional context 57 | description: Add screenshots etc. (Anything that will provide more context about the problem) 58 | validations: 59 | required: false -------------------------------------------------------------------------------- /src/synth.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "notes.hh" 11 | 12 | 13 | struct SynthNote { 14 | SynthNote(): note(24), begin(), length() {} 15 | SynthNote(const Note& n): note(n.note), begin(n.begin), length(n.length()) {} 16 | bool operator<(const SynthNote& rhs) { return begin < rhs.begin; } 17 | int note; 18 | double begin; 19 | double length; 20 | }; 21 | 22 | typedef QList SynthNotes; 23 | 24 | /** 25 | * @brief Threaded WAV buffer creator. 26 | * 27 | * Synthesizes and schedules notes in a thread and sends them to the main thread when its time to play them. 28 | */ 29 | class Synth: public QThread 30 | { 31 | Q_OBJECT 32 | public: 33 | static const int SampleRate = 22050; ///< Sample rate 34 | 35 | Synth(QObject *parent = NULL) : QThread(parent), m_delay(), m_pos(), m_rate(1.0), m_noteBegin(), m_curBuffer(), m_quit() 36 | { 37 | qRegisterMetaType("QByteArray"); // Register type for use with queued connections 38 | } 39 | ~Synth() { stop(); wait(); } 40 | 41 | /// Updates the synth 42 | void tick(qint64 pos, qreal playbackRate, const SynthNotes& notes); 43 | /// Stop synthesizing 44 | void stop(); 45 | /// Creates the sound 46 | static void createBuffer(QByteArray &buffer, int note, double length); 47 | 48 | signals: 49 | void playBuffer(const QByteArray&); 50 | 51 | protected: 52 | /// Thread runs here 53 | void run(); 54 | 55 | private: 56 | /// Calculates the next values 57 | void calcNext(); 58 | /// WAV header writer 59 | static std::string writeWavHeader(unsigned bits, unsigned ch, unsigned sr, unsigned samples); 60 | 61 | SynthNotes m_notes; ///< Notes to synthesize 62 | double m_delay; ///< How many seconds until the next sound must be played 63 | double m_pos; ///< Position where we are now 64 | double m_rate; ///< Music playback speed multiplier 65 | double m_noteBegin; ///< Position of the next note 66 | QByteArray m_soundData[2]; ///< The WAV buffers 67 | int m_curBuffer; ///< Which buffer we are currently using 68 | bool m_quit; ///< Flag to signal the thread should quit 69 | QMutex m_mutex; ///< Mutex for protecting resource access 70 | QWaitCondition m_condition; ///< For signaling the thread 71 | }; 72 | 73 | 74 | /** 75 | * @brief Class for playing a WAV buffer from memory. 76 | * 77 | * Designed to be reused, but won't play the buffer if the previous hasn't finished. 78 | */ 79 | class BufferPlayer: public QObject 80 | { 81 | Q_OBJECT 82 | Q_DISABLE_COPY(BufferPlayer) 83 | public: 84 | BufferPlayer(QObject *parent); 85 | 86 | bool play(const QByteArray& ba); 87 | 88 | public slots: 89 | void handleStateChanged(QAudio::State newState); 90 | void debugDumpStats(); 91 | 92 | private: 93 | QBuffer *m_buffer; 94 | QAudioOutput *m_player; 95 | }; 96 | -------------------------------------------------------------------------------- /docs/Design.txt: -------------------------------------------------------------------------------- 1 | The implementation is based on Qt as planned earlier and we will 2 | continue with this approach if no serious issues are encountered. We 3 | also considered an implementation as a feature of Performous and decided 4 | to use this as a fallback in case Qt turns out to be problematic. 5 | 6 | We also reviewed Editor on Fire but determined that it is not suitable 7 | for our use because it is primarily designed for entering guitar notes 8 | and a lot of manual labor is required becaus of that. Also the 9 | implementation is in rather unmaintainable C code (instead of clean C++) 10 | and the user-interface is not very good. 11 | 12 | Instead of separate modes for creating new song or editing an existing 13 | song we aim to provide all tools in the main editor view. This allows to 14 | user to choose his workflow in a more flexible manner and one can also 15 | go back and redo parts of an existing song using any of the tools 16 | available. 17 | 18 | Lyrics are imported in the format commonly used on lyric sites (text 19 | with sentence per line). Next the user can give timing information 20 | (beginning time) for each word e.g. by tapping space while listening to 21 | the song. Not all words need to be timed and anything that is not timed 22 | will float freely and all floating words will be evenly divided into the 23 | time period they take (between words that have been already timed). This 24 | allows the user not to time each word but instead time only what needs 25 | to be timed (e.g. each sentence) and the rest will be done 26 | automatically. 27 | 28 | Pitch detection is used to find the pitch and then the exact timing is 29 | determined from the detected pitch. The user may adjust pitch and timing 30 | (beginning, end) manually to correct possible problems. Here again it 31 | was planned to have autodetected parameters float, i.e. have them change 32 | flexibly if anything in the song is changed, until locked down by user's 33 | manual adjustments. 34 | 35 | It is initially planned to display the flexibly created notes as soon as 36 | lyrics are available so that the user will technically be fixing a song 37 | rather than creating one. This should allow doing the minimal amount of 38 | work for getting the job done. 39 | 40 | As we won't be able to store all the required metadata about flexible 41 | parameters and such in SingStar XML nor other established formats, we 42 | will implement our own project file format that contains all the actions 43 | taken for creating the song (this also allows for implementing unlimited 44 | undo/redo). 45 | 46 | SingStar XML requires a constant BPM value which is not realistic for 47 | most songs because BPM usually varies. We considered implementing a beat 48 | detector for getting the actual beat timing but this would not work for 49 | all songs and we would have to use a bogus value for SingStar XML 50 | anyway. Because BPM is not relevant for singing, we plan to simply use a 51 | bogus value and not even try to detect the beats. 52 | 53 | -------------------------------------------------------------------------------- /src/notes.cc: -------------------------------------------------------------------------------- 1 | #include "notes.hh" 2 | 3 | #include "util.hh" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | QString MusicalScale::getNoteStr(double freq) const { 10 | int id = getNoteId(freq); 11 | if (id == -1) return QString(); 12 | static const char * note[12] = {"C ","C#","D ","D#","E ","F ","F#","G ","G#","A ","A#","B "}; 13 | QString buf; 14 | QTextStream ts(&buf); 15 | // Acoustical Society of America Octave Designation System 16 | //int octave = 2 + id / 12; 17 | ts << note[id%12] << " " << int(round(freq)) << " Hz"; 18 | return ts.readAll(); 19 | } 20 | 21 | unsigned int MusicalScale::getNoteNum(int id) const { 22 | // C major scale 23 | int n = id % 12; 24 | return (n + (n > 4)) / 2; 25 | } 26 | 27 | bool MusicalScale::isSharp(int id) const { 28 | id %= 12; 29 | if (id < 0) id += 12; // Fix the modulus of a negative value 30 | // C major scale 31 | switch (id) { 32 | case 1: case 3: case 6: case 8: case 10: return true; 33 | } 34 | return false; 35 | } 36 | 37 | double MusicalScale::getNoteFreq(int id) const { 38 | if (id == -1) return 0.0; 39 | return m_baseFreq * std::pow(2.0, (id - m_baseId) / 12.0); 40 | } 41 | 42 | int MusicalScale::getNoteId(double freq) const { 43 | double note = getNote(freq); 44 | if (note >= 0.0 && note < 100.0) return int(note + 0.5); 45 | return -1; 46 | } 47 | 48 | double MusicalScale::getNote(double freq) const { 49 | if (freq < 1.0) return getNaN(); 50 | return m_baseId + 12.0 * std::log(freq / m_baseFreq) / std::log(2.0); 51 | } 52 | 53 | double MusicalScale::getNoteOffset(double freq) const { 54 | double frac = freq / getNoteFreq(getNoteId(freq)); 55 | return 12.0 * std::log(frac) / std::log(2.0); 56 | } 57 | 58 | Duration::Duration(): begin(getNaN()), end(getNaN()) {} 59 | 60 | 61 | const Note::Type Note::types[] = { NORMAL, GOLDEN, FREESTYLE, SLIDE, SLEEP, TAP, HOLDBEGIN, HOLDEND, ROLL, MINE, LIFT }; 62 | 63 | Note::Note(QString lyric): syllable(lyric), begin(), end(), phase(getNaN()), type(NORMAL), note(), notePrev(), lineBreak() {} 64 | 65 | double Note::diff(double note, double n) { return remainder(n - note, 12.0); } 66 | 67 | int Note::getTypeInt() const { 68 | switch (type) { 69 | case NORMAL: return 0; 70 | case GOLDEN: return 1; 71 | case FREESTYLE: return 2; 72 | case SLIDE: return 3; 73 | case SLEEP: return 4; 74 | default: return 255; 75 | } 76 | } 77 | 78 | QString Note::typeString() const { 79 | switch (type) { 80 | case NORMAL: return QT_TR_NOOP("Normal"); 81 | case GOLDEN: return QT_TR_NOOP("Bonus"); 82 | case FREESTYLE: return QT_TR_NOOP("Freestyle"); 83 | case SLIDE: return QT_TR_NOOP("Slide"); 84 | case SLEEP: return QT_TR_NOOP("Sleep"); 85 | default: return QT_TR_NOOP("Unknown"); 86 | } 87 | } 88 | 89 | 90 | VocalTrack::VocalTrack(QString name) : name(name) {reload();} 91 | 92 | void VocalTrack::reload() { 93 | notes.clear(); 94 | m_scoreFactor = 0.0; 95 | noteMin = std::numeric_limits::max(); 96 | noteMax = std::numeric_limits::min(); 97 | beginTime = endTime = getNaN(); 98 | } 99 | -------------------------------------------------------------------------------- /platform/mingw-cross-env/mk/qt.mk: -------------------------------------------------------------------------------- 1 | # This file is part of mingw-cross-env. 2 | # See doc/index.html for further information. 3 | 4 | # Qt 5 | PKG := qt 6 | $(PKG)_IGNORE := 7 | $(PKG)_VERSION := 4.7.1 8 | $(PKG)_CHECKSUM := fcf764d39d982c7f84703821582bd10c3192e341 9 | $(PKG)_SUBDIR := $(PKG)-everywhere-opensource-src-$($(PKG)_VERSION) 10 | $(PKG)_FILE := $(PKG)-everywhere-opensource-src-$($(PKG)_VERSION).tar.gz 11 | $(PKG)_WEBSITE := http://qt.nokia.com/ 12 | $(PKG)_URL := http://get.qt.nokia.com/qt/source/$($(PKG)_FILE) 13 | $(PKG)_DEPS := gcc libodbc++ postgresql freetds openssl libgcrypt zlib libpng jpeg libmng tiff sqlite libiconv dbus 14 | 15 | define $(PKG)_UPDATE 16 | wget -q -O- 'http://qt.gitorious.org/qt/qt/commits' | \ 17 | grep '
  • > ${GITHUB_ENV} 24 | 25 | - name: Comment on PR 26 | uses: actions/github-script@v5 27 | with: 28 | # This snippet is public-domain, taken from 29 | # https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml 30 | # and modified to allow comments on external PRs 31 | script: | 32 | async function upsertComment(owner, repo, issue_number, purpose, body) { 33 | const {data: comments} = await github.rest.issues.listComments( 34 | {owner, repo, issue_number}); 35 | const marker = ``; 36 | body = marker + "\n" + body; 37 | const existing = comments.filter((c) => c.body.includes(marker)); 38 | if (existing.length > 0) { 39 | const last = existing[existing.length - 1]; 40 | core.info(`Updating comment ${last.id}`); 41 | await github.rest.issues.updateComment({ 42 | owner, repo, 43 | body, 44 | comment_id: last.id, 45 | }); 46 | } else { 47 | core.info(`Creating a comment in issue / PR ${issue_number}`); 48 | await github.rest.issues.createComment({issue_number, body, owner, repo}); 49 | } 50 | } 51 | const {owner, repo} = context.repo; 52 | const run_id = '${{github.event.workflow_run.id}}'; 53 | const artifacts = await github.paginate( 54 | github.rest.actions.listWorkflowRunArtifacts, {owner, repo, run_id}); 55 | if (!artifacts.length) { 56 | return core.error(`No artifacts found`); 57 | } 58 | let body = `Download the artifacts for this pull request:\n`; 59 | for (const art of artifacts) { 60 | body += `\n* [${art.name}.zip](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`; 61 | } 62 | body += ` \n\nThis service is provided by [nightly.link](https://github.com/oprypin/nightly.link). These artifacts will expire in 90 days and will not be available for download after that time.`; 63 | core.info("Review thread message body:", body); 64 | await upsertComment(owner, repo, ${{ env.PR_NUM }}, 65 | "nightly-link", body); 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Composer 2 | ======== 3 | 4 | Composer is a song editor for creating (and converting) notes for music games in various formats. It attempts to make the process easy by automating as much as possible while providing a simple and attractive interface to do the remaining manual work. 5 | 6 | Homepage: http://performous.org/composer 7 | 8 | Features 9 | -------- 10 | 11 | * Song pitch analysis based on the esteemed algorithms from Performous. 12 | * Zoomable interface to quickly get an overview or doing very precise timing. 13 | * Possibility to synthesize the notes to get a feel of their "sound". 14 | * Import/export in various formats including: 15 | - SingStar XML 16 | - UltraStar TXT 17 | - Frets on Fire MIDI 18 | - LRC 19 | 20 | Latest builds 21 | ========== 22 | - [Linux - Ubuntu 20.04](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-ubuntu_20.04.deb.zip) 23 | - [Linux - Ubuntu 22.04](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-ubuntu_22.04.deb.zip) 24 | - [Linux - Ubuntu 24.04](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-ubuntu_24.04.deb.zip) 25 | - [Linux - Debian 12](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-debian_12.deb.zip) 26 | - [Linux - Fedora 36](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_36.rpm.zip) 27 | - [Linux - Fedora 37](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_37.rpm.zip) 28 | - [Linux - Fedora 38](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_38.rpm.zip) 29 | - [Linux - Fedora 39](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_39.rpm.zip) 30 | - [Linux - Fedora 40](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_40.rpm.zip) 31 | - [Linux - Fedora 41](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest-fedora_41.rpm.zip) 32 | - [Linux - AppImage](https://nightly.link/performous/composer/workflows/build_and_release/master/Composer-latest.AppImage.zip) 33 | 34 | Build & Install 35 | -------- 36 | 37 | For building the master branch you will need: 38 | Qt5: 39 | Core, Gui, Widgets, Network, XML, Multimedia, Multimedia-widgets, Multimedia-plugins and Platform files 40 | 41 | FFmpeg/Libav: 42 | LibAVCodec, LibAVUtil, LibAVFormat, LibSWResample 43 | 44 | Other: 45 | Zlib, Xvid, ssleay, libxml, libx264, libvorbis, libtheora, libspeex, libpng, libstdc++, libpcre, 46 | libopus, libopencore, libogg, libnettle, libmp3lame, liblzma, libintl, libiconv, libhogweed, libharfbuzz, libgnutls, libgmp, glib, libgcc, libfreetype, libeay32, libbz2, libbluray, icuuc, icuin, icudt. 47 | 48 | Build for linux: 49 | To build for linux simply install the required libraries through your distribution's package manager along with CMake. Then create a build folder and use cmake (or cmake-gui) to generate your makefiles. Then make && make install (last command might require root privileges). 50 | 51 | Build for Windows: 52 | To build for Windows simply install the required libraries through vcpkg. Then startup Visual Studio and let cmake generate your makefiles. Then build the project and make it run. 53 | -------------------------------------------------------------------------------- /src/operation.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | struct Operation 11 | { 12 | enum OperationFlags { NORMAL = 0, NO_EXEC = 1, NO_EMIT = 2, NO_UPDATE = 4, SELECT_NEW = 8 }; 13 | 14 | Operation() { } 15 | Operation(const QString &opString) { *this << opString; } 16 | Operation(const QString &opString, int id) { *this << opString << id; } 17 | Operation(const QString &opString, int id, bool state) { *this << opString << id << state; } 18 | Operation(const QString &opString, const QString &str1, const QString &str2) { *this << opString << str1 << str2; } 19 | 20 | // Functions to add parameters to Operation 21 | 22 | Operation& operator<<(const QString &str) { m_params.push_back(QVariant(str)); return *this; } 23 | Operation& operator<<(int i) { m_params.push_back(QVariant(i)); return *this; } 24 | Operation& operator<<(bool b) { m_params.push_back(QVariant(b)); return *this; } 25 | Operation& operator<<(float f) { m_params.push_back(QVariant(f)); return *this; } 26 | Operation& operator<<(double d) { m_params.push_back(QVariant(d)); return *this; } 27 | Operation& operator<<(QVariant q) { m_params.push_back(q); return *this; } 28 | 29 | /// Get the operation id 30 | QString op() const { return m_params.isEmpty() ? "" : m_params.front().toString(); } 31 | /// Get parameter count (excluding operation id) 32 | int paramCount() const { return m_params.size() - 1; } 33 | 34 | /// Overloaded template getter for param at certain position (1-based) 35 | template 36 | T param(int index) const { validate(index); m_params[index].value(); } 37 | 38 | // Get Operation parameter at certain index (1-based) 39 | 40 | QString s(int index) const { validate(index); return m_params[index].toString(); } 41 | char c(int index) const { validate(index); return m_params[index].toChar().toLatin1(); } 42 | int i(int index) const { validate(index); return m_params[index].toInt(); } 43 | unsigned u(int index) const { validate(index); return m_params[index].toUInt(); } 44 | bool b(int index) const { validate(index); return m_params[index].toBool(); } 45 | float f(int index) const { validate(index); return m_params[index].toFloat(); } 46 | double d(int index) const { validate(index); return m_params[index].toDouble(); } 47 | QVariant q(int index) const { validate(index); return m_params[index]; } 48 | 49 | /// Array access for modifying param 50 | QVariant& operator[](int index) { validate(index); return m_params[index]; } 51 | 52 | std::string dump() const { 53 | QString st; 54 | QTextStream ts(&st); 55 | foreach(QVariant qv, m_params) 56 | ts << qv.toString() << " "; 57 | return st.toStdString(); 58 | } 59 | 60 | friend QDataStream& operator<<(QDataStream&, const Operation&); 61 | friend QDataStream& operator>>(QDataStream& stream, Operation& op); 62 | 63 | private: 64 | void validate(int index) const { 65 | if (index < 0 || index >= m_params.size()) 66 | throw std::runtime_error("Invalid access to operation parameters"); 67 | } 68 | 69 | QList m_params; 70 | }; 71 | 72 | typedef QStack OperationStack; 73 | 74 | // Serialization operators 75 | QDataStream& operator<<(QDataStream& stream, const Operation& op); 76 | QDataStream& operator>>(QDataStream& stream, Operation& op); 77 | -------------------------------------------------------------------------------- /src/notelabel.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "notes.hh" 7 | #include "operation.hh" 8 | 9 | /** 10 | * @brief Widget representing a single note. 11 | * 12 | * Notes: 13 | * - Is rather useless without a parent NoteGraphWidget-object 14 | * - Widget is initially hidden and without a pixmap to allow quick creation 15 | * - Pixmap updates are generally delayed a little 16 | * - The idea is to allow some time to apply the base operation to every note 17 | * and then do the gfx updates asynchronously 18 | * - NoteLabel has its own mouse handling for moving, resizing, cursors, tooltips etc, 19 | * but requires the parent NoteGraphWidget to update some internal states 20 | * - Geometry & position is calculated from the underlying Note attributes (i.e. time and pitch) 21 | * - Setting size or pos manually will be overridden so the Note must be manipulated instead 22 | * - NoteLabel can be serialized to Operation-class 23 | */ 24 | class NoteLabel: public QLabel 25 | { 26 | Q_OBJECT 27 | 28 | public: 29 | static const int render_delay; 30 | static const int resize_margin; 31 | static const double default_length; 32 | static const double min_length; 33 | 34 | NoteLabel(const Note ¬e, QWidget *parent, bool floating = true); 35 | 36 | QString lyric() const { return m_note.syllable; } 37 | void setLyric(const QString &text) { m_note.syllable = text; QTimer::singleShot(render_delay, this, SLOT(updatePixmap())); } 38 | QString description(bool multiline) const; 39 | 40 | bool isSelected() const { return m_selected; } 41 | void setSelected(bool state = true); 42 | 43 | Note& note() { return m_note; } 44 | Note note() const { return m_note; } 45 | void updateLabel(); 46 | void updateTips(); 47 | 48 | bool isFloating() const { return m_floating; } 49 | void setFloating(bool state) { m_floating = state; QTimer::singleShot(render_delay, this, SLOT(updatePixmap())); } 50 | bool isLineBreak() const { return m_note.lineBreak; } 51 | void setLineBreak(bool state) { m_note.lineBreak = state; QTimer::singleShot(render_delay, this, SLOT(updatePixmap())); } 52 | void setType(int newtype) { m_note.type = Note::types[newtype]; QTimer::singleShot(render_delay, this, SLOT(updatePixmap())); } 53 | 54 | void startResizing(int dir); 55 | void startDragging(const QPoint& point); 56 | 57 | /// Create Operation from NoteLabel 58 | operator Operation() const; 59 | 60 | bool operator<(const NoteLabel &rhs) const { return m_note.begin < rhs.note().begin; } 61 | 62 | public slots: 63 | /// Shows the widget and creates the pixmap; if already visible, do nothing 64 | /// @return true if pixmap was actually created, false if the widget was already visible 65 | bool createPixmap() { if (isVisible()) return false; show(); updatePixmap(); return true; } 66 | /// Updates the pixmap but only if the widget is visible (i.e. createPixmap has been called) 67 | void updatePixmap(); 68 | 69 | protected: 70 | void resizeEvent(QResizeEvent *event); 71 | void moveEvent(QMoveEvent *event); 72 | void mouseMoveEvent(QMouseEvent *event); 73 | void closeEvent(QCloseEvent *event) { deleteLater(); event->accept(); } 74 | 75 | private: 76 | Note m_note; 77 | bool m_selected; 78 | bool m_floating; 79 | int m_resizing; 80 | QPoint m_hotspot; 81 | }; 82 | 83 | bool inline cmpNoteLabelPtr(const NoteLabel *lhs, const NoteLabel *rhs) 84 | { 85 | return (*lhs) < (*rhs); 86 | } 87 | -------------------------------------------------------------------------------- /src/songparser.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "song.hh" 4 | #include 5 | 6 | namespace SongParserUtil { 7 | /// Parse a boolean from string and assign it to a variable 8 | void assign(bool& var, QString const& str); 9 | } 10 | 11 | /// parses songfiles 12 | class SongParser { 13 | public: 14 | /// constructor 15 | SongParser(Song& s); 16 | 17 | static bool looksLikeSongFile(QString const& data) { 18 | return txtCheck(data) || xmlCheck(data) || iniCheck(data) || smCheck(data) || lrcCheck(data); 19 | } 20 | 21 | private: 22 | void finalize(); 23 | 24 | Song& m_song; 25 | QTextStream m_stream; 26 | unsigned int m_linenum; 27 | bool getline(QString& line); 28 | bool m_relative; 29 | double m_gap; 30 | 31 | // UltraStar TXT 32 | static bool txtCheck(QString const& data); 33 | void txtParse(); 34 | bool txtParseField(QString const& line); 35 | bool txtParseNote(QString line, VocalTrack &vocal); 36 | 37 | // SingStar XML 38 | static bool xmlCheck(QString const& data); 39 | void xmlParse(); 40 | 41 | // Frets on Fire MIDI 42 | static bool iniCheck(QString const& data); 43 | static bool midiCheck(QString const& data); 44 | void iniParse(); 45 | void iniParseField(QString const& line); 46 | void midParse(); 47 | 48 | // LRC / Soramimi 49 | static bool lrcCheck(QString const& data); 50 | void lrcParse(); 51 | bool lrcNoteParse(QString line, VocalTrack &vocal); 52 | double convertLRCTimestampToDouble(QString timeStamp); 53 | 54 | // FIXME: Dummy funcs 55 | static bool smCheck(QString const& data) { (void)data; return false; } 56 | void smParse() { } 57 | bool smParseField(std::string line) { (void)line; return false; } 58 | Notes smParseNotes(std::string line) { (void)line; return Notes(); } 59 | 60 | double m_prevtime; 61 | unsigned int m_prevts; 62 | unsigned int m_relativeShift; 63 | double m_maxScore; 64 | struct BPM { 65 | BPM(double _begin, double _ts, double _bpm, double division): begin(_begin), step(60.0 / _bpm / division), ts(_ts) {} 66 | double begin; // Time in seconds 67 | double step; // Seconds per quarter note 68 | double ts; 69 | }; 70 | typedef std::vector bpms_t; 71 | bpms_t m_bpms; 72 | unsigned m_tsPerBeat; ///< The ts increment per beat 73 | unsigned m_tsEnd; ///< The ending ts of the song 74 | void addBPM(double ts, double bpm, double division = 4.0) { 75 | if (!(bpm >= 1.0 && bpm < 1e12)) throw std::runtime_error("Invalid BPM value"); 76 | if (!m_bpms.empty()) { 77 | double diff = ts - m_bpms.back().ts; 78 | if (diff == 0.0) m_bpms.pop_back(); // Avoid zero-length BPM definitions 79 | else if (!(diff > 0.0)) throw std::runtime_error("Invalid BPM timestamp"); 80 | } 81 | m_bpms.push_back(BPM(tsTime(ts), ts, bpm, division)); 82 | } 83 | /// Convert a timestamp (beats) into time (seconds) 84 | double tsTime(double ts) const { 85 | if (m_bpms.empty()) { 86 | if (ts != 0) throw std::runtime_error("BPM data missing"); 87 | return m_gap; 88 | } 89 | for (std::vector::const_reverse_iterator it = m_bpms.rbegin(); it != m_bpms.rend(); ++it) { 90 | if (it->ts <= ts) return it->begin + (ts - it->ts) * it->step; 91 | } 92 | throw std::logic_error("INTERNAL ERROR: BPM data invalid"); 93 | } 94 | /// Stops stored in format 95 | Song::Stops m_stops; 96 | /// Convert a stop into (as stored in the song) 97 | std::pair stopConvert(std::pair s) { 98 | s.first = tsTime(s.first); 99 | return s; 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /src/song.cc: -------------------------------------------------------------------------------- 1 | #include "song.hh" 2 | #include "songparser.hh" 3 | #include "notes.hh" 4 | #include "util.hh" 5 | #include 6 | #include 7 | 8 | void Song::reload(bool errorIgnore) { 9 | loadStatus = NONE; 10 | vocalTracks.clear(); 11 | //instrumentTracks.clear(); 12 | //danceTracks.clear(); 13 | beats.clear(); 14 | midifilename.clear(); 15 | category.clear(); 16 | genre.clear(); 17 | edition.clear(); 18 | title.clear(); 19 | artist.clear(); 20 | collateByTitle.clear(); 21 | collateByTitleOnly.clear(); 22 | collateByArtist.clear(); 23 | collateByArtistOnly.clear(); 24 | text.clear(); 25 | creator.clear(); 26 | music.clear(); 27 | cover.clear(); 28 | background.clear(); 29 | video.clear(); 30 | videoGap = 0.0; 31 | start = 0.0; 32 | preview_start = getNaN(); 33 | bpm = 0.0; 34 | hasBRE = false; 35 | b0rkedTracks = false; 36 | if (!filename.isEmpty()) { 37 | try { SongParser(*this); } catch (...) { if (!errorIgnore) throw; } 38 | } 39 | collateUpdate(); 40 | } 41 | 42 | void Song::dropNotes() { 43 | // Singing 44 | if (!vocalTracks.empty()) { 45 | for (VocalTracks::iterator it = vocalTracks.begin(); it != vocalTracks.end(); ++it) 46 | it->second.notes.clear(); 47 | } 48 | /* 49 | // Instruments 50 | if (!instrumentTracks.empty()) { 51 | for (InstrumentTracks::iterator it = instrumentTracks.begin(); it != instrumentTracks.end(); ++it) 52 | it->second.nm.clear(); 53 | } 54 | // Dancing 55 | if (!danceTracks.empty()) { 56 | for (DanceTracks::iterator it = danceTracks.begin(); it != danceTracks.end(); ++it) 57 | it->second.clear(); 58 | } 59 | */ 60 | b0rkedTracks = false; 61 | loadStatus = HEADER; 62 | } 63 | 64 | void Song::collateUpdate() { 65 | collateByTitle = collate(title + artist) + '\0' + filename; 66 | collateByTitleOnly = collate(title); 67 | collateByArtist = collate(artist + title) + '\0' + filename; 68 | collateByArtistOnly = collate(artist); 69 | } 70 | 71 | QString Song::collate(QString const& str) { 72 | return str; //unicodeCollate(str); 73 | } 74 | 75 | namespace { 76 | // Cannot simply take double as its second argument because of a C++ defect 77 | bool noteEndLessThan(Note const& a, Note const& b) { return a.end < b.end; } 78 | } 79 | 80 | Song::Status Song::status(double time) { 81 | Note target; target.end = time; 82 | Notes::const_iterator it = std::lower_bound(getVocalTrack().notes.begin(), getVocalTrack().notes.end(), target, noteEndLessThan); 83 | if (it == getVocalTrack().notes.end()) return FINISHED; 84 | if (it->begin > time + 4.0) return INSTRUMENTAL_BREAK; 85 | return NORMAL; 86 | } 87 | 88 | bool Song::getNextSection(double pos, SongSection §ion) { 89 | if (songsections.empty()) return false; 90 | for (std::vector::iterator it= songsections.begin(); it != songsections.end(); ++it) { 91 | if (it->begin > pos) { 92 | section = *it; 93 | return true; 94 | } 95 | } 96 | // returning false here will jump forward 5s (see screen_sing.cc) 97 | return false; 98 | } 99 | 100 | bool Song::getPrevSection(double pos, SongSection §ion) { 101 | if (songsections.empty()) return false; 102 | for (std::vector::reverse_iterator it= songsections.rbegin(); it != songsections.rend(); it++) { 103 | // subtract 1 second so we can jump across a section 104 | if (it->begin < pos - 1.0) { 105 | section = *it; 106 | return true; 107 | } 108 | } 109 | // returning false here will jump backwards by 5s (see screen_sing.cc) 110 | return false; 111 | } 112 | -------------------------------------------------------------------------------- /.github/workflows/appimage.yml: -------------------------------------------------------------------------------- 1 | name: Build AppImage Packages 2 | 3 | on: 4 | # Run when called from other workflows 5 | workflow_call: 6 | inputs: 7 | package_complete_version: 8 | description: 'The output of the complete_version of the "determine_version" job from the build_and_release.yml workflow' 9 | required: true 10 | type: string 11 | release_upload_url: 12 | description: 'The output of the "create_release" job from the build_and_release.yml workflow' 13 | required: true 14 | type: string 15 | 16 | jobs: 17 | # Create the AppImage 18 | AppImage: 19 | name: Create the AppImage 20 | runs-on: ubuntu-22.04 21 | steps: 22 | - name: Install the AppImage bundler and Performous deps 23 | id: fetch_deps 24 | run: | 25 | wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.0.0-beta.1/appimage-builder-1.0.0-677acbd-x86_64.AppImage 26 | chmod +x appimage-builder-x86_64.AppImage 27 | sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder 28 | sudo apt update 29 | sudo apt-get install -y --no-install-recommends git cmake build-essential gettext help2man libavcodec-dev libavformat-dev libswscale-dev qtbase5-dev qtmultimedia5-dev ca-certificates file libfuse2 30 | 31 | - name: Checkout Git 32 | id: checkout_git 33 | uses: actions/checkout@v4 34 | 35 | - name: Build the AppImage 36 | id: build_appimage 37 | run: | 38 | # Pull in our common build functions 39 | . .github/workflows/build_functions.sh 40 | 41 | PACKAGE_VERSION=${{ inputs.package_complete_version }} 42 | sed -i s/@@VERSION@@/${PACKAGE_VERSION}/ AppImageBuilder.yml 43 | mkdir build 44 | cd build 45 | cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DENABLE_WEBSERVER=ON -DENABLE_WEBCAM=ON -DPERFORMOUS_VERSION=$PACKAGE_VERSION .. 46 | make -j$(nproc) install DESTDIR=../AppDir 47 | cd .. 48 | appimage-builder --recipe AppImageBuilder.yml --skip-test 49 | 50 | ## Provided by the common build functions 51 | package_name "$(pwd)" "*.AppImage" "${PACKAGE_VERSION}" 52 | 53 | # Upload artifacts during pull-requests 54 | - name: Upload artifact 55 | uses: actions/upload-artifact@v4 56 | if: ${{ github.event_name == 'pull_request' }} 57 | with: 58 | name: ${{ env.ARTIFACT_NAME }} 59 | path: ${{ env.ARTIFACT_PATH }} 60 | 61 | # Upload artifacts on master 62 | - name: Upload artifact with unified name 63 | if: ${{ github.ref == 'refs/heads/master' }} 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: ${{ env.MASTER_ARTIFACT_NAME }} 67 | path: ${{ env.MASTER_ARTIFACT_PATH }} 68 | 69 | # Upload artifacts to releases only during Release events 70 | - name: Upload artifacts to tagged release 71 | id: upload_assets 72 | if: ${{ github.event_name != 'pull_request' && github.ref_type == 'tag' }} 73 | uses: actions/upload-release-asset@v1 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | with: 77 | upload_url: ${{ inputs.release_upload_url }} 78 | asset_path: ${{ env.ARTIFACT_PATH }} 79 | asset_name: ${{ env.ARTIFACT_NAME }} 80 | asset_content_type: application/octet-stream 81 | -------------------------------------------------------------------------------- /src/libda/sample.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | /** 6 | * @file sample.hpp Sample format definition and format conversions. 7 | */ 8 | 9 | namespace da { 10 | 11 | // Implement mathematical rounding (which C++ unfortunately currently lacks) 12 | template T round(T val) { return static_cast(static_cast(val + (val >= 0 ? 0.5 : -0.5))); } 13 | 14 | // WARNING: changing this breaks binary compatibility on the library! 15 | typedef float sample_t; 16 | 17 | // A helper function for clamping a value to a certain range 18 | template T clamp(T val, T min, T max) { 19 | if (val < min) val = min; 20 | if (val > max) val = max; 21 | return val; 22 | } 23 | 24 | const sample_t max_s16 = 32767.0f, min_s16 = -max_s16 - 1.0f; 25 | const sample_t max_s24 = 8388607.0f, min_s24 = -max_s24 - 1.0f; 26 | const sample_t max_s32 = 2147483647.0f, min_s32= -max_s32 - 1.0f; 27 | 28 | // The following conversions provide lossless conversions between floats 29 | // and integers. Be sure to use only these conversions or otherwise the 30 | // conversions may not be lossless, due to different scaling factors being 31 | // used by different libraries. 32 | 33 | // The negative minimum integer value produces sample_t value slightly 34 | // more negative than -1.0 but this is necessary in order to prevent 35 | // clipping in the float-to-int conversions. Now amplitude 1.0 in floating 36 | // point produces -32767 .. 32767 symmetrical non-clipping range in s16. 37 | 38 | static inline sample_t conv_from_s16(int s) { return s / max_s16; } 39 | static inline sample_t conv_from_s24(int s) { return s / max_s24; } 40 | static inline sample_t conv_from_s32(int s) { return s / max_s32; } 41 | // The rounding is strictly not necessary, but it greatly improves 42 | // the error tolerance if any floating point calculations are done. 43 | // The ugly static_casts are required to avoid warnings in MSVC. 44 | static inline int conv_to_s16(sample_t s) { return clamp(static_cast(round(s * max_s16)), static_cast(min_s16), static_cast(max_s16)); } 45 | static inline int conv_to_s24(sample_t s) { return clamp(static_cast(round(s * max_s24)), static_cast(min_s24), static_cast(max_s24)); } 46 | static inline int conv_to_s32(sample_t s) { return static_cast(clamp(round(s * max_s32), min_s32, max_s32 )); } 47 | // Non-rounding non-clamping versions are provided for very low end devices (still lossless) 48 | static inline int conv_to_s16_fast(sample_t s) { return static_cast(s * max_s16); } 49 | static inline int conv_to_s24_fast(sample_t s) { return static_cast(s * max_s24); } 50 | static inline int conv_to_s32_fast(sample_t s) { return static_cast(s * max_s32); } 51 | 52 | template class step_iterator: public std::iterator { 53 | ValueType* m_pos; 54 | std::ptrdiff_t m_step; 55 | public: 56 | step_iterator(ValueType* pos, std::ptrdiff_t step): m_pos(pos), m_step(step) {} 57 | ValueType& operator*() { return *m_pos; } 58 | step_iterator operator+(std::ptrdiff_t rhs) { return step_iterator(m_pos + m_step * rhs, m_step); } 59 | step_iterator& operator++() { m_pos += m_step; return *this; } 60 | step_iterator operator++(int) { step_iterator ret = *this; ++*this; return ret; } 61 | bool operator!=(step_iterator const& rhs) const { return m_pos != rhs.m_pos; } 62 | std::ptrdiff_t operator-(step_iterator const& rhs) const { return (m_pos - rhs.m_pos) / m_step; } 63 | // TODO: more operators 64 | }; 65 | 66 | typedef step_iterator sample_iterator; 67 | typedef step_iterator sample_const_iterator; 68 | } 69 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [our Discord server](https://discord.gg/NS3m3ad) by private messaging one of the admins around. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | The Contributor Covenant is © 2014 Coraline Ada Ehmke and 76 | published under CC-BY-4.0. 77 | -------------------------------------------------------------------------------- /src/libda/fft.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /** 4 | * @file fft.hpp FFT and related facilities. 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #ifndef M_PI 12 | #define M_PI 3.141592653589793 13 | #endif 14 | 15 | namespace da { 16 | 17 | namespace math { 18 | 19 | /** Calculate the square of val. **/ 20 | static inline double sqr(double val) { return val * val; } 21 | 22 | template struct SinCosSeries { 23 | static double value() { 24 | return 1 - sqr(A * M_PI / B) / M / (M+1) * SinCosSeries::value(); 25 | } 26 | }; 27 | template struct SinCosSeries { 28 | static double value() { return 1.0; } 29 | }; 30 | 31 | template struct Sin { 32 | static double value() { return (A * M_PI / B) * SinCosSeries<2, 34, B, A>::value(); } 33 | }; 34 | 35 | template struct Cos { 36 | static double value() { return SinCosSeries<1, 33, B, A>::value(); } 37 | }; 38 | 39 | /** Calculate sin(2 pi A / B). **/ 40 | template double sin() { return Sin::value(); } 41 | 42 | /** Calculate cos(2 pi A / B). **/ 43 | template double cos() { return Cos::value(); } 44 | } 45 | 46 | namespace fourier { 47 | // Based on the description of Volodymyr Myrnyy in 48 | // http://www.dspdesignline.com/showArticle.jhtml?printableArticle=true&articleId=199903272 49 | template struct DanielsonLanczos { 50 | static void apply(std::complex* data) { 51 | const std::size_t N = 1 << P; 52 | const std::size_t M = N / 2; 53 | // Compute even and odd halves 54 | DanielsonLanczos

    ().apply(data); 55 | DanielsonLanczos

    ().apply(data + M); 56 | // Combine the results 57 | using math::sqr; 58 | using math::sin; 59 | const std::complex wp(-2.0 * sqr(sin<1, N>()), -sin<2, N>()); 60 | std::complex w(1.0); 61 | for (std::size_t i = 0; i < M; ++i) { 62 | std::complex temp = data[i + M] * w; 63 | data[M + i] = data[i] - temp; 64 | data[i] += temp; 65 | w += w * wp; 66 | } 67 | } 68 | }; 69 | 70 | template struct DanielsonLanczos<0, T> { static void apply(std::complex*) {} }; 71 | } 72 | 73 | /** Perform FFT on data. **/ 74 | template void fft(std::complex* data) { 75 | // Perform bit-reversal sorting of sample data. 76 | const std::size_t N = 1 << P; 77 | std::size_t j = 0; 78 | for (std::size_t i = 0; i < N; ++i) { 79 | if (i < j) std::swap(data[i], data[j]); 80 | std::size_t m = N / 2; 81 | while (m > 1 && m <= j) { j -= m; m >>= 1; } 82 | j += m; 83 | } 84 | // Do the actual calculation 85 | fourier::DanielsonLanczos::apply(data); 86 | } 87 | 88 | /** Perform FFT on data from floating point iterator, windowing the input. **/ 89 | template std::vector > fft(InIt begin, Window window) { 90 | std::vector > data(1 << P); 91 | // Perform bit-reversal sorting of sample data. 92 | const std::size_t N = 1 << P; 93 | std::size_t j = 0; 94 | for (std::size_t i = 0; i < N; ++i) { 95 | data[j] = *begin++ * window[i]; 96 | std::size_t m = N / 2; 97 | while (m > 1 && m <= j) { j -= m; m >>= 1; } 98 | j += m; 99 | } 100 | // Do the actual calculation 101 | fourier::DanielsonLanczos::apply(&data[0]); 102 | return data; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/notes.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | /// musical scale, defaults to C major 9 | class MusicalScale { 10 | private: 11 | double m_baseFreq; 12 | static const int m_baseId = 33; 13 | 14 | public: 15 | /// constructor 16 | MusicalScale(double baseFreq = 440.0): m_baseFreq(baseFreq) {} 17 | /// get name of note 18 | QString getNoteStr(double freq) const; 19 | /// get note number for id 20 | unsigned int getNoteNum(int id) const; 21 | /// true if sharp note 22 | bool isSharp(int id) const; 23 | /// get frequence for note id 24 | double getNoteFreq(int id) const; 25 | /// get note id for frequence 26 | int getNoteId(double freq) const; 27 | /// get note for frequence 28 | double getNote(double freq) const; 29 | /// get note offset for frequence 30 | double getNoteOffset(double freq) const; 31 | /// get octave number 32 | 33 | /// get base id 34 | static int getBaseId() { return m_baseId; } 35 | }; 36 | 37 | 38 | /// stores duration of a note 39 | struct Duration { 40 | double begin, ///< beginning timestamp in seconds 41 | end; ///< ending timestamp in seconds 42 | Duration(); 43 | /// create a new Duration object and initialize begin and end 44 | Duration(double b, double e): begin(b), end(e) {} 45 | /// compares begin timestamps of two Duration structs 46 | static bool ltBegin(Duration const& a, Duration const& b) { return a.begin < b.begin; } 47 | /// compares end timestamps of two Duration structs 48 | static bool ltEnd(Duration const& a, Duration const& b) { return a.end < b.end; } 49 | }; 50 | 51 | typedef std::vector Durations; 52 | typedef std::map NoteMap; 53 | 54 | /// note read from songfile 55 | struct Note { 56 | Note(QString lyric = ""); 57 | /// note type - NOTE! Keep the types array below in sync with the enum! 58 | enum Type { FREESTYLE = 'F', NORMAL = ':', GOLDEN = '*', SLIDE = '+', SLEEP = '-', 59 | TAP = '1', HOLDBEGIN = '2', HOLDEND = '3', ROLL = '4', MINE = 'M', LIFT = 'L'} type; 60 | static const Type types[]; 61 | int getTypeInt() const; 62 | 63 | //Duration duration; ///< note begin/end 64 | double begin; // FIXME: Should use duration but it is pain to change everywhere 65 | double end; 66 | double phase; ///< position within a measure, [0, 1) 67 | int note; ///< MIDI pitch of the note (at the end for slide notes) 68 | int notePrev; ///< MIDI pitch of the previous note (should be same as note for everything but SLIDE) 69 | QString syllable; ///< lyrics syllable for that note 70 | bool lineBreak; ///< is this note ending a syllable? 71 | /// move beginning without changing length 72 | void move(double newBegin) { double l = length(); begin = newBegin; end = newBegin + l; } 73 | /// note length 74 | double length() const { return end - begin; } 75 | /// difference of n from note 76 | double diff(double n) const { return diff(note, n); } 77 | /// difference of n from note, so that note + diff(note, n) is n (mod 12) 78 | static double diff(double note, double n); 79 | /// compares begin of two notes 80 | static bool ltBegin(Note const& a, Note const& b) { return a.begin < b.begin; } 81 | /// compares end of two notes 82 | static bool ltEnd(Note const& a, Note const& b) { return a.end < b.end; } 83 | 84 | /// human-readable description of note type 85 | QString typeString() const; 86 | }; 87 | 88 | typedef std::vector Notes; 89 | 90 | struct VocalTrack { 91 | VocalTrack(QString name); 92 | void reload(); 93 | QString name; 94 | Notes notes; 95 | int noteMin, noteMax; ///< lowest and highest note 96 | double beginTime, endTime; ///< the period where there are notes 97 | double m_scoreFactor; ///< normalization factor for the scoring system 98 | MusicalScale scale; ///< scale in which song is sung 99 | }; 100 | 101 | typedef std::map VocalTracks; 102 | -------------------------------------------------------------------------------- /src/textcodecselector.hh: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** 3 | ** Copyright (C) 2000-2008 TROLLTECH ASA. All rights reserved. 4 | ** 5 | ** This file is part of the Opensource Edition of the Qtopia Toolkit. 6 | ** 7 | ** This software is licensed under the terms of the GNU General Public 8 | ** License (GPL) version 2. 9 | ** 10 | ** See http://www.trolltech.com/gpl/ for GPL licensing information. 11 | ** 12 | ** Contact info@trolltech.com if any conditions of this licensing are 13 | ** not clear to you. 14 | ** 15 | ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE 16 | ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. 17 | ** 18 | ** Code has been modified from the original. 19 | ****************************************************************************/ 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #include 31 | 32 | class TextCodecSelector : public QDialog { 33 | Q_OBJECT 34 | public: 35 | TextCodecSelector(QWidget* parent = 0) 36 | : QDialog(parent) 37 | { 38 | setWindowTitle(tr("Unknown encoding")); 39 | list = new QListWidget(this); 40 | connect(list, SIGNAL(itemActivated(QListWidgetItem*)), this, SLOT(accept())); 41 | connect(list, SIGNAL(itemPressed(QListWidgetItem*)), this, SLOT(accept())); 42 | codecs = QTextCodec::availableCodecs(); 43 | qSort(codecs); 44 | list->addItem(tr("Automatic")); 45 | foreach (QByteArray n, codecs) { 46 | list->addItem(n); 47 | } 48 | QLabel *label = new QLabel(tr("Choose the encoding for this file:")); 49 | label->setWordWrap(true); 50 | QVBoxLayout *vb = new QVBoxLayout(this); 51 | vb->addWidget(label); 52 | vb->addWidget(list); 53 | setLayout(vb); 54 | } 55 | 56 | QTextCodec *selection(QByteArray ba) const 57 | { 58 | int n = list->currentRow(); 59 | if (n < 0) { 60 | return 0; 61 | } else if (n == 0) { 62 | // Automatic... just try them all and pick the one that can convert 63 | // in and out without losing anything with the smallest intermediate length. 64 | QTextCodec *best = 0; 65 | int shortest = INT_MAX; 66 | foreach (QByteArray name, codecs) { 67 | QTextCodec* c = QTextCodec::codecForName(name); 68 | QString enc = c->toUnicode(ba); 69 | if (c->fromUnicode(enc) == ba) { 70 | std::cout << QString(c->name()).toStdString() << std::endl; 71 | if (enc.length() < shortest) { 72 | best = c; 73 | shortest = enc.length(); 74 | } 75 | } 76 | } 77 | return best; 78 | } else { 79 | return QTextCodec::codecForName(codecs.at(n-1)); 80 | } 81 | } 82 | 83 | static QTextCodec* codecForContent(QByteArray ba, QWidget *parent = 0) 84 | { 85 | TextCodecSelector tcs(parent); 86 | tcs.setModal(true); 87 | tcs.exec(); 88 | if (tcs.result()) 89 | return tcs.selection(ba); 90 | return 0; 91 | } 92 | 93 | static QString readAllAndHandleEncoding(QFile &file, QWidget *parent = 0) 94 | { 95 | QString data = ""; 96 | QByteArray ba = file.readAll(); 97 | if (!ba.isEmpty()) { 98 | data = QString::fromUtf8(ba, ba.size()); 99 | if (data.toUtf8().size() != ba.size()) { 100 | // Not UTF-8 :( 101 | data = QString::fromLatin1(ba, ba.size()); 102 | if (data.toLatin1().size() != ba.size()) { 103 | // Not Latin1 :( 104 | data = QString::fromLocal8Bit(ba, ba.size()); 105 | if (data.toLocal8Bit().size() != ba.size()) { 106 | // Not Local 8-bit :( 107 | QTextCodec* codec = codecForContent(ba, parent); 108 | if (codec) data = codec->toUnicode(ba); 109 | } 110 | } 111 | } 112 | } 113 | return data; 114 | } 115 | 116 | private: 117 | QListWidget *list; 118 | QList codecs; 119 | }; 120 | -------------------------------------------------------------------------------- /platform/packaging.cmake: -------------------------------------------------------------------------------- 1 | set(CPACK_PACKAGE_NAME "${CMAKE_PROJECT_NAME}") 2 | set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") 3 | set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Application for creating song notes for pitch-detecting karaoke games.") 4 | set(CPACK_PACKAGE_CONTACT "http://performous.org/") 5 | set(CPACK_SOURCE_IGNORE_FILES 6 | "/.cvsignore" 7 | "/.gitignore" 8 | "/songs/" 9 | "/build/" 10 | "/.svn/" 11 | "/.git/" 12 | ) 13 | set(CPACK_PACKAGE_EXECUTABLES composer) 14 | set(CPACK_SOURCE_GENERATOR "TBZ2") 15 | set(CPACK_GENERATOR "TBZ2") 16 | 17 | # Debian specific settings 18 | set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) 19 | set(CPACK_DEBIAN_PACKAGE_SECTION "Games") 20 | 21 | if("${CMAKE_BUILD_TYPE}" MATCHES "Release") 22 | set(CPACK_STRIP_FILES TRUE) 23 | endif("${CMAKE_BUILD_TYPE}" MATCHES "Release") 24 | 25 | if(APPLE) 26 | set(CPACK_GENERATOR "PACKAGEMAKER;OSXX11") 27 | endif(APPLE) 28 | if(UNIX) 29 | # Try to find architecture 30 | execute_process(COMMAND uname -m OUTPUT_VARIABLE CPACK_PACKAGE_ARCHITECTURE) 31 | string(STRIP "${CPACK_PACKAGE_ARCHITECTURE}" CPACK_PACKAGE_ARCHITECTURE) 32 | # Try to find distro name and distro-specific arch 33 | execute_process(COMMAND lsb_release -is OUTPUT_VARIABLE LSB_ID) 34 | execute_process(COMMAND lsb_release -rs OUTPUT_VARIABLE LSB_RELEASE) 35 | string(STRIP "${LSB_ID}" LSB_ID) 36 | string(STRIP "${LSB_RELEASE}" LSB_RELEASE) 37 | set(LSB_DISTRIB "${LSB_ID}${LSB_RELEASE}") 38 | if(NOT LSB_DISTRIB) 39 | set(LSB_DISTRIB "unix") 40 | endif(NOT LSB_DISTRIB) 41 | # For Debian-based distros we want to create DEB packages. 42 | if("${LSB_DISTRIB}" MATCHES "Ubuntu|Debian") 43 | set(CPACK_GENERATOR "DEB") 44 | set(CPACK_DEBIAN_PACKAGE_PRIORITY "extra") 45 | set(CPACK_DEBIAN_PACKAGE_SECTION "universe/editors") 46 | set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "performous") 47 | # We need to alter the architecture names as per distro rules 48 | if("${CPACK_PACKAGE_ARCHITECTURE}" MATCHES "i[3-6]86") 49 | set(CPACK_PACKAGE_ARCHITECTURE i386) 50 | endif("${CPACK_PACKAGE_ARCHITECTURE}" MATCHES "i[3-6]86") 51 | if("${CPACK_PACKAGE_ARCHITECTURE}" MATCHES "x86_64") 52 | set(CPACK_PACKAGE_ARCHITECTURE amd64) 53 | endif("${CPACK_PACKAGE_ARCHITECTURE}" MATCHES "x86_64") 54 | 55 | string(TOLOWER "${CPACK_PACKAGE_NAME}_${CPACK_PACKAGE_VERSION}-${LSB_DISTRIB}_${CPACK_PACKAGE_ARCHITECTURE}" CPACK_PACKAGE_FILE_NAME) 56 | endif("${LSB_DISTRIB}" MATCHES "Ubuntu|Debian") 57 | # For Fedora-based distros we want to create RPM packages. 58 | if("${LSB_DISTRIB}" MATCHES "Fedora") 59 | set(CPACK_GENERATOR "RPM") 60 | set(CPACK_RPM_PACKAGE_NAME "${CMAKE_PROJECT_NAME}") 61 | set(CPACK_RPM_PACKAGE_VERSION "${PROJECT_VERSION}") 62 | set(CPACK_RPM_PACKAGE_RELEASE "1") 63 | # set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games") 64 | set(CPACK_RPM_PACKAGE_LICENSE "GPLv3+") 65 | set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "TODO") 66 | set(CPACK_RPM_PACKAGE_DESCRIPTION "TODO") 67 | # We need to alter the architecture names as per distro rules 68 | if("${CPACK_PACKAGE_ARCHITECTURE}" MATCHES "i[3-6]86") 69 | set(CPACK_PACKAGE_ARCHITECTURE i386) 70 | endif("${CPACK_PACKAGE_ARCHITECTURE}" MATCHES "i[3-6]86") 71 | if("${CPACK_PACKAGE_ARCHITECTURE}" MATCHES "x86_64") 72 | set(CPACK_PACKAGE_ARCHITECTURE amd64) 73 | endif("${CPACK_PACKAGE_ARCHITECTURE}" MATCHES "x86_64") 74 | # Set the dependencies based on the distro version 75 | if("${LSB_DISTRIB}" MATCHES "Fedora14") 76 | #set(CPACK_RPM_PACKAGE_REQUIRES "TODO") 77 | endif("${LSB_DISTRIB}" MATCHES "Fedora14") 78 | if("${LSB_DISTRIB}" MATCHES "Fedora13") 79 | #set(CPACK_RPM_PACKAGE_REQUIRES "TODO") 80 | endif("${LSB_DISTRIB}" MATCHES "Fedora13") 81 | if(NOT CPACK_RPM_PACKAGE_REQUIRES) 82 | message("WARNING: ${LSB_DISTRIB} is not supported.\nPlease set deps in packaging.cmake before packaging.") 83 | endif(NOT CPACK_RPM_PACKAGE_REQUIRES) 84 | endif("${LSB_DISTRIB}" MATCHES "Fedora") 85 | set(CPACK_SYSTEM_NAME "${LSB_DISTRIB}-${CPACK_PACKAGE_ARCHITECTURE}") 86 | message(STATUS "Detected ${CPACK_SYSTEM_NAME}. Use make package to build packages (${CPACK_GENERATOR}).") 87 | endif(UNIX) 88 | 89 | include(CPack) 90 | -------------------------------------------------------------------------------- /src/songparser-lrc.cc: -------------------------------------------------------------------------------- 1 | #include "songparser.hh" 2 | #include 3 | #include 4 | 5 | // LRC has many variations: http://en.wikipedia.org/wiki/LRC_(file_format) 6 | // We also support Soramimi format, which differs as follows: 7 | // * Per word timing, [] instead of <> (in contrast to "Enhanced LRC") 8 | // * Centisecond separator is ':' instead of '.' 9 | 10 | 11 | bool SongParser::lrcCheck(QString const& data) { 12 | return data[0] == '['; 13 | } 14 | 15 | void SongParser::lrcParse() { 16 | VocalTrack vocal(TrackName::LEAD_VOCAL); 17 | Notes& notes = vocal.notes; 18 | QString line; 19 | 20 | while (getline(line)) { 21 | // LRC header tags 22 | if (line.startsWith("[ar:", Qt::CaseInsensitive)) { 23 | m_song.artist = line.mid(4).trimmed().remove(QRegExp("\\]$")); 24 | } else if (line.startsWith("[ti:", Qt::CaseInsensitive)) { 25 | m_song.title = line.mid(4).trimmed().remove(QRegExp("\\]$")); 26 | } else if (line.startsWith("[by:", Qt::CaseInsensitive)) { 27 | m_song.creator = line.mid(4).trimmed().remove(QRegExp("\\]$")); 28 | // TODO: Gap, [offset: ? 29 | } else if (line.length() >= 2 && line[1].isLetter()) { 30 | // Skip unknown header tags 31 | continue; 32 | } else { // Note parsing 33 | // These replacements are compatibility between Soramimi and "enhanced" LRC 34 | line.replace("<", "[").replace(">", "]"); 35 | lrcNoteParse(line, vocal); 36 | } 37 | } 38 | 39 | if (!notes.empty()) { 40 | vocal.beginTime = notes.front().begin; 41 | vocal.endTime = notes.back().end; 42 | // Insert notes 43 | m_song.insertVocalTrack(TrackName::LEAD_VOCAL, vocal); 44 | } else throw std::runtime_error(QT_TR_NOOP("Couldn't find any notes")); 45 | } 46 | 47 | bool SongParser::lrcNoteParse(QString line, VocalTrack& vocal) { 48 | if (line.isEmpty()) return false; 49 | if (line[0] != '[') throw std::runtime_error("Unexpected character at line start"); 50 | 51 | Notes& notes = vocal.notes; 52 | bool parsingTime = true, createNote = false; 53 | QString timeStr = "", lyric = ""; 54 | double time = 0, prevTime = 0; 55 | // Start traversing from 1 because we already know 0 is '[' 56 | for (int i = 1; i < line.length(); ++i) { 57 | // Two state parser: either parsing timestamp or lyrics 58 | if (parsingTime) { 59 | if (line[i].toLatin1() == ']') { // Timestamp end 60 | parsingTime = false; 61 | prevTime = time; 62 | time = convertLRCTimestampToDouble(timeStr); 63 | timeStr.clear(); 64 | if (!lyric.isEmpty()) { 65 | createNote = true; 66 | } else if (!notes.empty()) { 67 | // Adjust last sentence's end to fix possible overlaps if there is no line end timestamp 68 | notes.back().end = std::min(notes.back().end, time); 69 | } 70 | } else { // Accumulate timestamp string 71 | timeStr += line[i]; 72 | } 73 | } else { // Lyrics parsing mode 74 | if (line[i].toLatin1() == '[') parsingTime = true; // Lyric end 75 | else lyric += line[i]; // Accumulate lyric string 76 | } 77 | 78 | // Create the note 79 | if ((createNote || i == line.length() - 1) && !lyric.isEmpty()) { 80 | Note n(lyric); 81 | n.begin = prevTime; 82 | n.end = time; 83 | n.note = 33; 84 | notes.push_back(n); 85 | lyric.clear(); 86 | createNote = false; 87 | } 88 | } 89 | 90 | // Add SLEEP to line end 91 | Note n; 92 | n.type = Note::SLEEP; 93 | n.begin = time; 94 | n.end = time; 95 | n.note = 33; 96 | notes.push_back(n); 97 | 98 | return true; 99 | } 100 | 101 | double SongParser::convertLRCTimestampToDouble(QString timeStamp) { 102 | bool ok = false; 103 | // This replacing is also LRC <-> Soramimi compatibility 104 | timeStamp.replace(QString(":"), QString(".")); 105 | QString minutes = timeStamp.mid(0,2); 106 | QString seconds = timeStamp.mid(3,5); 107 | double min = minutes.toDouble(&ok); 108 | if (!ok) throw std::runtime_error("Invalid minutes in timestamp " + timeStamp.toStdString()); 109 | double sec = seconds.toDouble(&ok); 110 | if (!ok) throw std::runtime_error("Invalid seconds in timestamp " + timeStamp.toStdString()); 111 | return min * 60 + sec; 112 | } 113 | -------------------------------------------------------------------------------- /src/songparser-xml.cc: -------------------------------------------------------------------------------- 1 | #include "songparser.hh" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | 9 | int ts = 0; 10 | int sleepts = -1; 11 | 12 | 13 | /// 'Magick' to check if this file looks like correct format 14 | bool SongParser::xmlCheck(QString const& data) 15 | { 16 | return (data[0] == '<' && data[1] == '?' && data[2] == 'x' && data[3] == 'm' && data[4] == 'l') 17 | || (data[0] == '<' && data[1] == 'M' && data[2] == 'E' && data[3] == 'L'); 18 | } 19 | 20 | void SongParser::xmlParse() 21 | { 22 | // Build DOM tree from the xml file 23 | QDomDocument doc("MELODY"); 24 | if (!doc.setContent(m_stream.readAll())) { 25 | throw std::runtime_error(QT_TR_NOOP("XML parse error")); } 26 | 27 | VocalTrack vocal(TrackName::LEAD_VOCAL); 28 | Notes& notes = vocal.notes; 29 | 30 | // Parse meta 31 | QDomElement root = doc.documentElement(); 32 | m_song.bpm = root.attribute("Tempo").toDouble(); 33 | if (m_song.bpm == 0) 34 | throw std::runtime_error(QT_TR_NOOP("Invalid tempo")); 35 | if (root.attribute("Resolution") == QString("Demisemiquaver")) 36 | m_song.bpm *= 2; 37 | addBPM(0, m_song.bpm); 38 | m_song.genre = root.attribute("Genre"); 39 | m_song.year = root.attribute("Year"); 40 | 41 | bool track_found = false; // FIXME: HACK: We only parse the first track 42 | 43 | // Loop through the child elements 44 | QDomElement elem = root.firstChildElement(); 45 | while (!elem.isNull()) { 46 | 47 | if (elem.tagName() == "TRACK") { 48 | // Track found 49 | if (track_found) break; // FIXME: HACK: We only parse the first track 50 | track_found = true; 51 | m_song.artist = elem.attribute("Artist"); 52 | 53 | } else if (elem.tagName() == "SENTENCE") { 54 | // Sentence found 55 | // Loop through the notes in the sentence 56 | QDomElement noteElem = elem.firstChildElement(); 57 | while (!noteElem.isNull()) { 58 | 59 | // We are only interested in NOTE elements 60 | if (noteElem.tagName() == "NOTE") { 61 | // Note found 62 | int length = noteElem.attribute("Duration").toInt(); 63 | unsigned int ts = m_prevts; 64 | 65 | // See if it is an actual note and not sleep 66 | QString lyric = noteElem.attribute("Lyric").isEmpty() 67 | ? noteElem.attribute("Rap") : noteElem.attribute("Lyric"); 68 | if (noteElem.attribute("MidiNote") != "0" || !lyric.isEmpty()) { 69 | // TODO: Prettify lyric? (as ss_extract) 70 | Note n(lyric); 71 | if (noteElem.attribute("Bonus") == QString("Yes")) 72 | n.type = Note::GOLDEN; 73 | else if (noteElem.attribute("FreeStyle") == QString("Yes")) 74 | n.type = Note::FREESTYLE; 75 | else 76 | n.type = Note::NORMAL; 77 | 78 | n.note = noteElem.attribute("MidiNote").toInt(); 79 | n.notePrev = n.note; // No slide notes 80 | n.begin = tsTime(ts); 81 | n.end = tsTime(ts + length); 82 | 83 | // Track note meta 84 | vocal.noteMin = std::min(vocal.noteMin, n.note); 85 | vocal.noteMax = std::max(vocal.noteMax, n.note); 86 | 87 | // Save note 88 | notes.push_back(n); 89 | } 90 | 91 | // Update time 92 | m_prevts += length; 93 | m_prevtime = tsTime(ts + length); 94 | } 95 | noteElem = noteElem.nextSiblingElement(); 96 | } 97 | 98 | // Now add sentence end indicator 99 | Note n; 100 | n.type = Note::SLEEP; 101 | n.note = 0; 102 | n.begin = m_prevtime; 103 | n.end = n.begin; 104 | notes.push_back(n); 105 | } 106 | elem = elem.nextSiblingElement(); 107 | } 108 | 109 | if (!notes.empty()) { 110 | vocal.beginTime = notes.front().begin; 111 | vocal.endTime = notes.back().end; 112 | // Insert notes 113 | m_song.insertVocalTrack(TrackName::LEAD_VOCAL, vocal); 114 | } else throw std::runtime_error(QT_TR_NOOP("Couldn't find any notes")); 115 | } 116 | 117 | 118 | /* 119 | // Some extra formatting to make lyrics look better (hyphen removal & whitespace) 120 | if (lyric.size() > 0 && lyric[lyric.size() - 1] == '-') { 121 | if (lyric.size() > 1 && lyric[lyric.size() - 2] == ' ') lyric.erase(lyric.size() - 2); 122 | else lyric[lyric.size() - 1] = '~'; 123 | } else { 124 | lyric += ' '; 125 | } 126 | */ 127 | -------------------------------------------------------------------------------- /src/ffmpeg.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "libda/sample.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | class AudioQueue { 14 | public: 15 | void reset() { 16 | QMutexLocker lock(&m_mutex); 17 | m_size = 0; 18 | m_needData.wakeOne(); 19 | m_needSpace.wakeOne(); 20 | } 21 | template void input(Iterator begin, Iterator end, double scale) { 22 | QMutexLocker lock(&m_mutex); 23 | unsigned count = end - begin; 24 | unsigned capacity = m_ring.size(); 25 | if (capacity < count) throw std::logic_error("AudioQueue input chunk is bigger than capacity"); 26 | while (capacity - m_size < count) m_needSpace.wait(&m_mutex); 27 | for (unsigned i = 0; i < count; ++i) { 28 | m_ring[m_position + m_size++] = *begin++ * scale; 29 | } 30 | m_needData.wakeOne(); 31 | } 32 | void setEof(bool eof = true) { 33 | QMutexLocker lock(&m_mutex); 34 | m_eof = eof; 35 | m_needData.wakeOne(); 36 | } 37 | bool output(std::vector& out) { 38 | QMutexLocker lock(&m_mutex); 39 | while (m_size == 0) { 40 | if (m_eof) return false; 41 | m_needData.wait(&m_mutex); 42 | } 43 | std::size_t outsz = out.size(); 44 | out.resize(outsz + m_size); 45 | da::sample_t* outptr = &out[outsz]; 46 | Ring::iterator b = m_ring.begin() + m_position, 47 | e = m_ring.begin() + (m_position + m_size) % m_ring.size(); 48 | if (e <= b) { 49 | // Copy the first part 50 | std::copy(b, m_ring.end(), outptr); 51 | outptr += m_ring.end() - b; 52 | b = m_ring.begin(); 53 | } 54 | // Copy the only/last part 55 | std::copy(b, e, outptr); 56 | m_size = 0; 57 | m_needSpace.wakeOne(); 58 | return true; 59 | } 60 | unsigned samplesPerSecond() const { return m_channels * m_rate; } 61 | void setRateChannels(unsigned rate, unsigned channels) { m_rate = rate; m_channels = channels; } 62 | unsigned getRate() { return m_rate; } 63 | unsigned getChannels() { return m_channels; } 64 | AudioQueue(unsigned capacity = 32768): m_ring(capacity), m_channels(), m_position(), m_size(), m_eof() {} 65 | 66 | private: 67 | QMutex m_mutex; 68 | QWaitCondition m_needData, m_needSpace; 69 | typedef std::vector Ring; 70 | Ring m_ring; 71 | unsigned m_rate; 72 | unsigned m_channels; 73 | unsigned m_position; 74 | unsigned m_size; 75 | bool m_eof; 76 | }; 77 | 78 | // ffmpeg forward declarations 79 | // ffmpeg forward declarations 80 | extern "C" { 81 | struct AVCodec; 82 | struct AVCodecContext; 83 | struct AVFormatContext; 84 | struct AVFrame; 85 | struct SwrContext; 86 | struct SwsContext; 87 | } 88 | 89 | struct AVFormatContextDeleter 90 | { 91 | void operator ()(AVFormatContext*); 92 | }; 93 | 94 | struct AVCodecContextDeleter 95 | { 96 | void operator ()(AVCodecContext*); 97 | }; 98 | 99 | struct SwrContextDeleter 100 | { 101 | void operator ()(SwrContext*); 102 | }; 103 | 104 | /// ffmpeg class 105 | class FFmpeg: public QThread { 106 | public: 107 | /// constructor 108 | FFmpeg(std::string const& file); 109 | ~FFmpeg(); 110 | /// Thread runs here, don't call directly 111 | void run(); 112 | /// Queue for audio 113 | AudioQueue audioQueue; 114 | /** Seek to the chosen time. Will block until the seek is done, if wait is true. **/ 115 | void seek(double time, bool wait = true); 116 | /// Duration 117 | double duration() const; 118 | bool terminating() const { return m_quit; } 119 | 120 | private: 121 | class eof_error: public std::exception {}; 122 | void seek_internal(); 123 | void open(); 124 | void decodeNextFrame(); 125 | std::string m_filename; 126 | unsigned int m_rate; 127 | volatile bool m_quit; 128 | volatile bool m_running; 129 | volatile bool m_eof; 130 | volatile double m_seekTarget; 131 | std::unique_ptr pFormatCtx; 132 | std::unique_ptr m_resampleContext; 133 | std::unique_ptr pAudioCodecCtx; 134 | 135 | int audioStream; 136 | static QMutex s_avcodec_mutex; // Used for avcodec_open/close (which use some static crap and are thus not thread-safe) 137 | }; 138 | 139 | -------------------------------------------------------------------------------- /platform/mingw-cross-env/makeinstaller.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This file has been modified from: 3 | # NSIS script generator for Performous. 4 | # Copyright (C) 2010 John Stumpo 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | import os 20 | import subprocess 21 | import sys 22 | 23 | try: 24 | makensis = subprocess.Popen([os.environ['MAKENSIS'], '-'], stdin=subprocess.PIPE) 25 | except KeyError: 26 | makensis = subprocess.Popen(['makensis', '-'], stdin=subprocess.PIPE) 27 | 28 | if not os.path.isdir('dist'): 29 | os.mkdir('dist') 30 | os.chdir('stage') 31 | 32 | app = 'Composer' 33 | version = '2.0' 34 | 35 | 36 | makensis.stdin.write(r'''!include "MUI2.nsh" 37 | 38 | !define APP "%(app)s" 39 | !define VERSION "%(version)s" 40 | 41 | Name "${APP} ${VERSION}" 42 | OutFile "dist\${APP}-${VERSION}.exe" 43 | 44 | SetCompressor /SOLID lzma 45 | 46 | ShowInstDetails show 47 | ShowUninstDetails show 48 | 49 | InstallDir "$PROGRAMFILES\${APP}" 50 | InstallDirRegKey HKLM "Software\${APP}" "" 51 | 52 | RequestExecutionLevel admin 53 | 54 | !insertmacro MUI_PAGE_WELCOME 55 | !insertmacro MUI_PAGE_DIRECTORY 56 | !insertmacro MUI_PAGE_INSTFILES 57 | !insertmacro MUI_PAGE_FINISH 58 | 59 | !insertmacro MUI_UNPAGE_WELCOME 60 | !insertmacro MUI_UNPAGE_CONFIRM 61 | !insertmacro MUI_UNPAGE_INSTFILES 62 | !insertmacro MUI_UNPAGE_FINISH 63 | 64 | !insertmacro MUI_LANGUAGE "English" 65 | 66 | Section 67 | ''' % {'app': app, 'version': version}) 68 | 69 | for root, dirs, files in os.walk('.'): 70 | makensis.stdin.write(' SetOutPath "$INSTDIR\\%s"\n' % root.replace('/', '\\')) 71 | for file in files: 72 | makensis.stdin.write(' File "%s"\n' % os.path.join('stage', root, file).replace('/', '\\')) 73 | 74 | makensis.stdin.write(r''' WriteRegStr HKLM "Software\${APP}" "" "$INSTDIR" 75 | WriteUninstaller "$INSTDIR\uninst.exe" 76 | SetShellVarContext all 77 | CreateDirectory "$SMPROGRAMS\${APP}" 78 | CreateShortcut "$SMPROGRAMS\${APP}\${APP}.lnk" "$INSTDIR\${APP}.exe" 79 | CreateShortcut "$SMPROGRAMS\${APP}\Uninstall.lnk" "$INSTDIR\uninst.exe" 80 | WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP}" "DisplayName" "${APP}" 81 | WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP}" "UninstallString" "$\"$INSTDIR\uninst.exe$\"" 82 | WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP}" "DisplayIcon" "$INSTDIR\${APP}.exe" 83 | WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP}" "DisplayVersion" "${VERSION}" 84 | SectionEnd 85 | 86 | Section Uninstall 87 | ''') 88 | 89 | for root, dirs, files in os.walk('.', topdown=False): 90 | for dir in dirs: 91 | makensis.stdin.write(' RmDir "$INSTDIR\\%s"\n' % os.path.join(root, dir).replace('/', '\\')) 92 | for file in files: 93 | makensis.stdin.write(' Delete "$INSTDIR\\%s"\n' % os.path.join(root, file).replace('/', '\\')) 94 | makensis.stdin.write(' RmDir "$INSTDIR\\%s"\n' % root.replace('/', '\\')) 95 | 96 | makensis.stdin.write(r''' Delete "$INSTDIR\uninst.exe" 97 | RmDir "$INSTDIR" 98 | SetShellVarContext all 99 | Delete "$SMPROGRAMS\${APP}\${APP}.lnk" 100 | Delete "$SMPROGRAMS\${APP}\Uninstall.lnk" 101 | RmDir "$SMPROGRAMS\${APP}" 102 | DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP}" 103 | DeleteRegKey /ifempty HKLM "Software\${APP}" 104 | SectionEnd 105 | ''') 106 | 107 | makensis.stdin.close() 108 | if makensis.wait() != 0: 109 | print >>sys.stderr, 'Installer compilation failed.' 110 | sys.exit(1) 111 | else: 112 | print 'Installer ready.' 113 | -------------------------------------------------------------------------------- /src/songwriter-ini.cc: -------------------------------------------------------------------------------- 1 | #include "songwriter.hh" 2 | 3 | #include "midifile.hh" 4 | #include "util.hh" 5 | #include 6 | 7 | void FoFMIDIWriter::writeMIDI() const { 8 | Notes const& notes = s.getVocalTrack().notes; 9 | if (notes.empty()) throw std::runtime_error("No notes"); 10 | double tempo = s.bpm > 0 ? s.bpm : 120.0; 11 | unsigned division = 256; // Allow for very precise timing 12 | unsigned endtc = round(tempo / 60.0 * division * notes.back().end); 13 | midifile::Writer writer(1, 2, division); 14 | using midifile::Event; 15 | writer.startTrack(); 16 | Event ev; 17 | ev.type = Event::SPECIAL; 18 | ev.channel = 0x0F; 19 | unsigned char buf[16]; 20 | ev.begin = ev.end = buf; 21 | // Write tempo info 22 | ev.arg1 = Event::META_TEMPO; 23 | ev.end = ev.begin + 3; 24 | unsigned val = 6e+7 / tempo; // Microseconds per beat 25 | buf[0] = val >> 16; 26 | buf[1] = val >> 8; 27 | buf[2] = val; 28 | writer.writeEvent(ev); 29 | // TODO: write Performous Composer and Title= & Artist= like FoF lyric converter does (note: it does on PART VOCALS but timing track seems more appropriate) 30 | // End the timing track 31 | ev.arg1 = Event::META_ENDOFTRACK; 32 | writer.writeEvent(ev); 33 | // Vocals track begins 34 | writer.startTrack(); 35 | // Write track name 36 | ev.arg1 = Event::META_SEQNAME; 37 | std::string partvocals = "PART VOCALS"; 38 | std::copy(partvocals.begin(), partvocals.end(), buf); 39 | ev.end = ev.begin + partvocals.size(); 40 | writer.writeEvent(ev); 41 | ev.begin = ev.end = NULL; 42 | // Write notes 43 | bool sentenceOn = false; // Sentence note 105 not playing 44 | unsigned timecode = 0; 45 | for (Notes::const_iterator it = notes.begin(), itend = notes.end(); it != itend; ++it) { 46 | ev.timecode = round(tempo / 60.0 * division * it->begin) - timecode; 47 | timecode += ev.timecode; 48 | if (it->lineBreak) { 49 | ev.type = Event::NOTE_ON; 50 | ev.channel = 0; 51 | ev.arg1 = 105; // Special note value for starting a new sentence 52 | // End the previous sentence (if not at the beginning) 53 | if (sentenceOn) { 54 | ev.arg2 = 0; 55 | writer.writeEvent(ev); // Note OFF 56 | ev.timecode = 0; // Reset time for the next event 57 | } 58 | sentenceOn = true; 59 | // Begin a new one 60 | ev.arg2 = 127; 61 | writer.writeEvent(ev); // Note ON 62 | ev.timecode = 0; // Reset time for the next event 63 | } 64 | // Write lyric 65 | QByteArray bytes = it->syllable.toUtf8(); 66 | ev.type = Event::SPECIAL; 67 | ev.channel = 0x0F; 68 | ev.arg1 = Event::META_LYRIC; 69 | ev.begin = reinterpret_cast(bytes.data()); 70 | ev.end = ev.begin + bytes.size(); 71 | writer.writeEvent(ev); 72 | ev.end = ev.begin = NULL; 73 | // Prepare for writing note on/off events 74 | ev.timecode = 0; // Same timecode as the lyric 75 | ev.type = Event::NOTE_ON; 76 | ev.channel = 0; 77 | // Note begin 78 | ev.arg1 = 36 + it->note; // FoF format uses offset for note values :( 79 | ev.arg2 = 127; // Note ON 80 | writer.writeEvent(ev); 81 | // Note end 82 | ev.timecode = round(tempo / 60.0 * division * it->end) - timecode; 83 | timecode += ev.timecode; 84 | ev.arg2 = 0; // Note OFF 85 | writer.writeEvent(ev); 86 | } 87 | ev.timecode = 0; 88 | // Terminate the sentence note 89 | if (sentenceOn) { 90 | ev.type = Event::NOTE_ON; 91 | ev.channel = 0; 92 | ev.arg1 = 105; 93 | ev.arg2 = 0; writer.writeEvent(ev); // Note OFF 94 | } 95 | // Terminate the vocal track 96 | ev.type = Event::SPECIAL; 97 | ev.channel = 0x0F; 98 | ev.arg1 = Event::META_ENDOFTRACK; 99 | writer.writeEvent(ev); 100 | // Write to file 101 | QByteArray name = (path + "/notes.mid").toLocal8Bit(); 102 | writer.save(std::string(name.data(), name.size()).c_str()); 103 | } 104 | 105 | void FoFMIDIWriter::writeINI() const { 106 | QFile f(path + "/song.ini"); 107 | if (!f.open(QFile::WriteOnly | QFile::Truncate)) 108 | throw std::runtime_error("Couldn't open target file"); 109 | 110 | QTextStream out(&f); 111 | out.setCodec("UTF-8"); 112 | 113 | out << "[song]\n"; 114 | out << "name = " << (s.title.isEmpty() ? "Unknown" : s.title) << '\n'; 115 | out << "artist = " << (s.artist.isEmpty() ? "Unknown" : s.artist) << '\n'; 116 | if (!s.genre.isEmpty()) out << "genre = " << s.genre << '\n'; 117 | if (!s.year.isEmpty()) out << "year = " << s.year << '\n'; 118 | } 119 | 120 | -------------------------------------------------------------------------------- /src/pitch.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "util.hh" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | static inline double level2dB(double level) { return 20.0 * std::log10(level); } 11 | static inline double dB2level(double db) { return std::pow(10.0, db / 20.0); } 12 | 13 | /// A tone is a collection of a base frequency (freq) and all its harmonics 14 | struct Tone { 15 | static const std::size_t MAXHARM = 16; ///< The maximum number of harmonics tracked 16 | double freq; ///< Frequency (Hz) 17 | double level; ///< Level (linear) 18 | double harmonics[MAXHARM]; ///< Harmonics' levels 19 | Tone(); 20 | bool operator==(double f) const; ///< Compare for rough frequency match 21 | // Linked list of a continuous tone 22 | Tone* prev; 23 | Tone* next; 24 | static bool cmpByLevel(Tone const& a, Tone const& b) { return a.level > b.level; } 25 | }; 26 | 27 | static inline bool operator==(Tone const& lhs, Tone const& rhs) { return lhs == rhs.freq; } 28 | static inline bool operator!=(Tone const& lhs, Tone const& rhs) { return !(lhs == rhs); } 29 | static inline bool operator<=(Tone const& lhs, Tone const& rhs) { return lhs.freq < rhs.freq || lhs == rhs; } 30 | static inline bool operator>=(Tone const& lhs, Tone const& rhs) { return lhs.freq > rhs.freq || lhs == rhs; } 31 | static inline bool operator<(Tone const& lhs, Tone const& rhs) { return lhs.freq < rhs.freq && lhs != rhs; } 32 | static inline bool operator>(Tone const& lhs, Tone const& rhs) { return lhs.freq > rhs.freq && lhs != rhs; } 33 | 34 | struct Moment { 35 | typedef std::list Tones; 36 | Tones m_tones; 37 | double m_time; 38 | Moment(double t); 39 | double time() const { return m_time; } 40 | void stealTones(Tones& tones); 41 | }; 42 | 43 | /// A peak contains information about a single frequency 44 | struct Peak { 45 | double freqFFT; 46 | double freq; 47 | double level; 48 | Peak(): freqFFT(), freq(), level() {} 49 | }; 50 | 51 | /// A combo combines multiple FFT peaks that all display the same frequency into one 52 | struct Combo { 53 | double freq; 54 | double level; 55 | Combo(): freq(), level() {} 56 | void combine(Peak const& p); 57 | bool match(double freqOther) const; 58 | static bool cmpByFreq(Combo const& a, Combo const& b) { return a.freq < b.freq; } 59 | static bool cmpByLevel(Combo const& a, Combo const& b) { return a.level > b.level; } 60 | }; 61 | 62 | 63 | /// analyzer class 64 | /** class to analyze input audio and transform it into useable data 65 | */ 66 | class Analyzer { 67 | public: 68 | typedef std::vector > Fourier; ///< FFT vector (the first level of detection) 69 | typedef std::vector Peaks; ///< Peaks (the second level of detection) 70 | typedef std::list Tones; ///< Tones (the final level of detection) 71 | typedef std::list Moments; ///< Time-serie history of time and tones 72 | /// constructor 73 | Analyzer(double rate, std::string id); 74 | /** Get the fourier transform. **/ 75 | Fourier const& getFourier() const { return m_fft; } 76 | /** Get the peak frequencies. **/ 77 | Peaks const& getPeaks() const { return m_peaks; } 78 | /** Get a list of all tones detected. **/ 79 | Moments const& getMoments() const { return m_moments; } 80 | /** Find a tone within the singing range; prefers strong tones around 200-400 Hz. **/ 81 | //Tone const* findTone(double minfreq = 70.0, double maxfreq = 700.0) const; 82 | std::string const& getId() const { return m_id; } 83 | /// Process processSize() samples from RndIt input 84 | template void process(RndIt input) { 85 | std::vector pcm(input, input + processSize()); // Needs local modifyable copy for calculations 86 | calcFFT(&pcm[0]); 87 | calcTones(); 88 | } 89 | unsigned processSize() const; ///< The number of samples required by process() 90 | unsigned processStep() const; ///< The number of samples to increment the input position after each call to process() 91 | double getTime() const { return m_moments.empty() ? 0.0 : m_moments.back().time(); } 92 | private: 93 | double m_rate; 94 | std::string m_id; 95 | std::vector m_window; 96 | Fourier m_fft; 97 | std::vector m_fftLastPhase; 98 | Peaks m_peaks; 99 | Moments m_moments; 100 | mutable double m_oldfreq; 101 | void calcFFT(float* pcm); 102 | void calcTones(); 103 | void temporalMerge(Tones& tones); 104 | }; 105 | 106 | -------------------------------------------------------------------------------- /docs/helpindex.html: -------------------------------------------------------------------------------- 1 |

    Help Browser

    2 |
      3 |
    1. Introduction
    2. 4 |
    3. 5 | Basic workflow 6 |
        7 |
      1. Importing song and lyrics
      2. 8 |
      3. Note timing
      4. 9 |
      5. Fine tuning
      6. 10 |
      7. Song metadata
      8. 11 |
      12 |
    4. 13 |
    5. Tips and tricks
    6. 14 |
    7. File formats
    8. 15 |
    16 | 17 | 18 |

    Introduction

    19 |

    This editor is designed to create notes for use in pitch analyzing karaoke games. We attempt to make the process as easy as possible by automating as much as we can. For example, the editor analyzes the song and attempts to automatically place the notes at the correct pitch.

    20 | 21 | 22 |

    Basic Workflow

    23 |

    In addition to the sections here, the intended workflow is documented as a handy dialog with clickable items - follow it step-by-step and in the end you'll have finished notes for the song. Access it through the main menubar: Help --> Getting started.

    24 | 25 |

    Importing song and lyrics

    26 |

    The first step is to import a music file for analyzation and lyrics text for generating notes. This can be done e.g. through the import menu. Supported song file formats vary depending on the platform, but at least mp3 and ogg should be ok. Lyrics assume that each phrase is on a line of its own - when the text is imported, a note is generated for each word and a sentence marker is placed at the beginning of each line.

    27 |

    You can also add an additional music file via the import menu. It will get analyzed and the results are displayed with different colors. The idea is that if you have a music file with both vocals and instruments, plus another karaoke version with just the instruments, you can use the analysis from the additional (karaoke) music to determine which tones belong to the background and are probably not singing.

    28 | 29 |

    Note timing

    30 |

    The easiest way to get the notes roughly timed is to switch to the Tools tab and start listening to the music. Each time you hear the phrase that is displayed in the tab, hit the Time phrase button (or its hotkey) and the start of the current phrase is placed to that position.

    31 |

    This way you'll be timing only the beginnings of the phrases - the rest of the notes are divided evenly to the extra space. Obviously this is not perfect, but it gives a very good starting point for fine tuning and timing each note by itself using this method would require very good reflexes and concentration.

    32 | 33 |

    Fine tuning

    34 |

    Once the notes are roughly timed, it is time to start manually tuning and fixing everything. Each time you correct a note, the neighbouring uncorrected ones adjust themselves accordingly, making your task easier.

    35 |

    At this phase, you can also break words into syllables in order to create more accurate notes. It is also possible to add or remove notes or batches of them.

    36 | 37 |

    Song metadata

    38 |

    The song title, artist and other metadata is automatically read from the music file if it happens to contain that information. If it is wrong or missing, you can fix it through the Song properties tab.

    39 | 40 | 41 |

    Tips and tricks

    42 |
      43 |
    • If possible, use vocals-only music tracks for best pitch analyzation (and thus auto-pitch) results.
    • 44 |
    • In addition to copy-pasting lyrics from clipboard or loading them from a text file, you can also drag and drop text directly from other applications (like a web browser) to the editor.
    • 45 |
    • Zoom (mouse wheel or ctrl+ -/+) in to get most precise timing or zoom out to get an overview and quickly change to other part of the song.
    • 46 |
    • The actions available through the menu bar and the tabs are also accessible through a context menu that opens up if you right click the note area.
    • 47 |
    48 | 49 | 50 |

    File formats

    51 |

    Various formats are supported to a reasonable extent:

    52 |
      53 |
    • "Native" project file format, which preserves floating note status and undo history.
    • 54 |
    • SingStar XML, import/export
    • 55 |
    • UltraStar TXT, import/export
    • 56 |
    • Frets on Fire INI+MIDI, import/export
    • 57 |
    • LRC and Soramimi karaoke lyrics (no pitch), import/export
    • 58 |
    • Plain text lyrics (no pitch), import/export
    • 59 |
    60 |

    Check the docs/FileFormats.txt for more detailed information about the level of support.

    61 | -------------------------------------------------------------------------------- /platform/mingw-cross-env/copydlls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # DLL dependency resolution and copying script. 3 | # Copyright (C) 2010 John Stumpo 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import os 19 | import shutil 20 | import struct 21 | import sys 22 | 23 | if len(sys.argv) != 3: 24 | sys.stderr.write('''Usage: %s [source] [destination] 25 | Copies DLLs in source needed by PE executables in destination to destination. 26 | Both source and destination should be directories. 27 | ''' % sys.argv[0]) 28 | sys.exit(1) 29 | 30 | def is_pe_file(file): 31 | f = open(file, 'rb') 32 | if f.read(2) != 'MZ': 33 | return False # DOS magic number not present 34 | f.seek(60) 35 | peoffset = struct.unpack(' 5 | #include 6 | #include 7 | 8 | namespace midifile { 9 | typedef uint8_t value_type; 10 | typedef uint8_t* iterator; 11 | typedef uint8_t const* const_iterator; 12 | 13 | struct Event { 14 | enum Type { 15 | NOTE_OFF = 0x80, 16 | NOTE_ON = 0x90, 17 | NOTE_AFTERTOUCH = 0xA0, 18 | CONTROLLER = 0xB0, 19 | PROGRAM_CHANGE = 0xC0, 20 | CHANNEL_AFTERTOUCH = 0xD0, 21 | PITCH_BEND = 0xE0, 22 | SPECIAL = 0xF0 23 | }; 24 | enum Meta { 25 | META_SEQNUMBER = 0, 26 | META_TEXT, 27 | META_COPYRIGHT, 28 | META_SEQNAME, 29 | META_INSTRNAME, 30 | META_LYRIC, 31 | META_MARKERTEXT, 32 | META_CUEPOINT, 33 | META_CHPREFIX = 0x20, 34 | META_ENDOFTRACK = 0x2F, 35 | META_TEMPO = 0x51, 36 | META_SMTPEOFFSET = 0x54, 37 | META_TIMESIGNATURE = 0x58, 38 | META_KEYSIGNATURE = 0x59, 39 | META_SEQUENCERSPECIFIC = 0x7F 40 | }; 41 | static char const* metaName(Meta meta); 42 | unsigned timecode; // Relative to previous event 43 | Type type; 44 | unsigned channel; 45 | unsigned arg1; 46 | unsigned arg2; 47 | const_iterator begin, end; // Data belonging to the event (including any terminating 0x7F) 48 | Meta getMeta() const { return static_cast(arg1); } 49 | std::string getDataStr() const { return std::string(begin, end); } 50 | void print() const; ///< Prints to std::cout (for debugging only) 51 | Event(): timecode(), type(), channel(), arg1(), arg2(), begin(), end() {} 52 | }; 53 | 54 | class Reader { 55 | public: 56 | static const unsigned margin = 20; ///< The minimum number of extra bytes required after the end of the buffer for more efficient processing 57 | /// MIDI reader, read header. 58 | Reader(char const* filename); 59 | /// Get the number of tracks, including the timing track if used in the current format 60 | unsigned numTracks() const { return m_tracks; } 61 | /// Start reading a track (or jump to the next track), must be called before parsing any events 62 | /// @returns false if end of file reached (all tracks processed) 63 | bool startTrack(); 64 | /// Parse the next event of the current track, returns false if at the end of track 65 | bool parseEvent(Event& ev); 66 | /// Get the number of timecode units in a beat (1/4 note) 67 | unsigned getDivision() const { return m_division; } 68 | private: 69 | void parseMThd(); 70 | void parseMTrk(); 71 | void parseRiff(char const* name); 72 | template unsigned read() { 73 | unsigned value = 0; 74 | for (unsigned i = N - 1; i < N; --i) value |= *m_pos++ << (8 * i); 75 | return value; 76 | } 77 | 78 | unsigned read_varlen() { 79 | unsigned value = 0; 80 | unsigned len = 0; 81 | unsigned c = 0; 82 | do { 83 | if (++len > 4) throw std::runtime_error("MIDI parse error (too long varlen)"); 84 | c = *m_pos++; 85 | value = (value << 7) | (c & 0x7F); 86 | } while (c & 0x80); 87 | return value; 88 | } 89 | std::vector m_data; ///< Only used when reading a file, not used if the user provides a buffer 90 | const_iterator m_pos; 91 | const_iterator m_riffEnd; 92 | const_iterator m_fileEnd; 93 | unsigned m_tracks; 94 | unsigned m_division; 95 | unsigned m_runningStatus; 96 | }; 97 | 98 | class Writer { 99 | public: 100 | /// MIDI file writer (constructs the output in a memory buffer) 101 | /// @param fmt MIDI format (use 1 if in doubt) 102 | /// @param tracks The number of tracks (with fmt 1 this is one more than the actual tracks) 103 | /// @param division How many timecode units fit into a beat (1/4 note) 104 | Writer(unsigned fmt, unsigned tracks, unsigned division); 105 | /// Flush the output to a file (after writing everything) 106 | void save(char const* filename); 107 | /// Start a new track, must be called before each track. 108 | /// Ends any previous track but won't automatically add end of track event. 109 | void startTrack(); 110 | /// Writes an event to current track 111 | void writeEvent(Event const& ev); 112 | private: 113 | void beginRiff(char const* name); 114 | void endRiff(); 115 | template void write(unsigned value) { 116 | for (unsigned i = N - 1; i < N; --i) m_data.push_back(value >> (8 * i)); 117 | } 118 | void write_varlen(unsigned value) { 119 | if (value >= 0x10000000U) throw std::logic_error("Value cannot be MIDI varlen encoded"); 120 | if (value >= 0x200000U) m_data.push_back(0x80 | (value >> 21)); 121 | if (value >= 0x4000U) m_data.push_back(0x80 | (value >> 14)); 122 | if (value >= 0x80U) m_data.push_back(0x80 | (value >> 7)); 123 | m_data.push_back(value & 0x7F); 124 | } 125 | std::vector m_data; 126 | unsigned m_riffBegin; 127 | }; 128 | } 129 | 130 | -------------------------------------------------------------------------------- /src/libda/portaudio.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /** 4 | * @file portaudio.hpp OOP / RAII wrappers & utilities for PortAudio library. 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "../unicode.hh" 14 | 15 | #define PORTAUDIO_CHECKED(func, args) portaudio::internal::check(func args, #func) 16 | 17 | namespace portaudio { 18 | class Error: public std::runtime_error { 19 | public: 20 | Error(PaError code_, char const* func_): 21 | runtime_error(std::string(func_) + " failed: " + Pa_GetErrorText(code_)), 22 | m_code(code_), 23 | m_func(func_) 24 | {} 25 | PaError code() const { return m_code; } 26 | std::string func() const { return m_func; } 27 | private: 28 | PaError m_code; 29 | char const* m_func; 30 | }; 31 | 32 | namespace internal { 33 | void inline check(PaError code, char const* func) { if (code != paNoError) throw Error(code, func); } 34 | } 35 | 36 | struct DeviceInfo { 37 | DeviceInfo(std::string n = "", int i = 0, int o = 0): name(n), in(i), out(o) {} 38 | std::string desc() { 39 | std::ostringstream oss; 40 | oss << name << " ("; 41 | if (in) oss << in << " in"; 42 | if (in && out) oss << ", "; 43 | if (out) oss << out << " out"; 44 | return oss.str() + ")"; 45 | } 46 | std::string name; 47 | int in, out; 48 | }; 49 | 50 | typedef std::vector DeviceInfos; 51 | 52 | struct AudioDevices { 53 | static int count() { return Pa_GetDeviceCount(); } 54 | /// Constructor gets the PA devices into a vector 55 | AudioDevices() { 56 | for (unsigned i = 0, end = Pa_GetDeviceCount(); i != end; ++i) { 57 | PaDeviceInfo const* info = Pa_GetDeviceInfo(i); 58 | if (!info) devices.push_back(DeviceInfo()); 59 | else devices.push_back(DeviceInfo(convertToUTF8(info->name), info->maxInputChannels, info->maxOutputChannels)); 60 | } 61 | } 62 | /// Get a printable dump of the devices 63 | std::string dump() { 64 | std::ostringstream oss; 65 | oss << "PortAudio devices:" << std::endl; 66 | for (unsigned i = 0; i < devices.size(); ++i) 67 | oss << " " << i << " " << devices[i].name << " (" << devices[i].in << " in, " << devices[i].out << " out)" << std::endl; 68 | oss << std::endl; 69 | return oss.str(); 70 | } 71 | DeviceInfos devices; 72 | }; 73 | 74 | struct Init { 75 | Init() { PORTAUDIO_CHECKED(Pa_Initialize, ()); } 76 | ~Init() { Pa_Terminate(); } 77 | }; 78 | 79 | struct Params { 80 | PaStreamParameters params; 81 | Params(PaStreamParameters const& init = PaStreamParameters()): params(init) { 82 | // Some useful defaults so that things just work 83 | channelCount(2).sampleFormat(paFloat32).suggestedLatency(0.05); 84 | } 85 | Params& channelCount(int val) { params.channelCount = val; return *this; } 86 | Params& device(PaDeviceIndex val) { params.device = val; return *this; } 87 | Params& sampleFormat(PaSampleFormat val) { params.sampleFormat = val; return *this; } 88 | Params& suggestedLatency(PaTime val) { params.suggestedLatency = val; return *this; } 89 | Params& hostApiSpecificStreamInfo(void* val) { params.hostApiSpecificStreamInfo = val; return *this; } 90 | operator PaStreamParameters const*() const { return ¶ms; } 91 | }; 92 | 93 | template int functorCallback(void const* input, void* output, unsigned long frameCount, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* userData) { 94 | return (*static_cast(userData))(input, output, frameCount, timeInfo, statusFlags); 95 | } 96 | 97 | class Stream { 98 | PaStream* m_handle; 99 | public: 100 | Stream( 101 | PaStreamParameters const* input, 102 | PaStreamParameters const* output, 103 | double sampleRate, 104 | unsigned long framesPerBuffer = paFramesPerBufferUnspecified, 105 | PaStreamFlags flags = paNoFlag, 106 | PaStreamCallback* callback = NULL, 107 | void* userData = NULL) 108 | { 109 | PORTAUDIO_CHECKED(Pa_OpenStream, (&m_handle, input, output, sampleRate, framesPerBuffer, flags, callback, userData)); 110 | } 111 | template Stream( 112 | Functor& functor, 113 | PaStreamParameters const* input, 114 | PaStreamParameters const* output, 115 | double sampleRate, 116 | unsigned long framesPerBuffer = paFramesPerBufferUnspecified, 117 | PaStreamFlags flags = paNoFlag) 118 | { 119 | PORTAUDIO_CHECKED(Pa_OpenStream, (&m_handle, input, output, sampleRate, framesPerBuffer, flags, functorCallback, &functor)); 120 | } 121 | ~Stream() { 122 | // Give audio a little time to shutdown but then just quit 123 | boost::thread audiokiller(Pa_CloseStream, m_handle); 124 | if (!audiokiller.timed_join(boost::posix_time::milliseconds(5000))) { 125 | std::cout << "PortAudio BUG: Pa_CloseStream hung for more than five seconds. Exiting program." << std::endl; 126 | exit(1); 127 | } 128 | } 129 | operator PaStream*() { return m_handle; } 130 | }; 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/songwriter-xml.cc: -------------------------------------------------------------------------------- 1 | #include "songwriter.hh" 2 | #include "util.hh" 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | void SingStarXMLWriter::writeXML() { 9 | if (tempo > 300) { 10 | tempo /= 2; 11 | res = "Demisemiquaver"; // Demisemiquaver = 2x tempo of Semiquaver 12 | } 13 | QDomDocument doc; 14 | QDomProcessingInstruction xmlheader = doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""); 15 | doc.appendChild(xmlheader); 16 | QDomElement root = doc.createElement("MELODY"); 17 | root.setAttribute("xmlns", "http://www.singstargame.com"); 18 | root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); 19 | root.setAttribute("Version", "1"); 20 | root.setAttribute("Tempo", QString::number(tempo)); 21 | root.setAttribute("FixedTempo", "Yes"); 22 | root.setAttribute("Resolution", res); 23 | root.setAttribute("Genre", s.genre); 24 | root.setAttribute("Year", s.year); 25 | root.setAttribute("xsi:schemaLocation", "http://www.singstargame.com http://15GMS-SINGSQL/xml_schema/melody.xsd"); 26 | root.setAttribute("m2xVersion", "060110"); //? 27 | root.setAttribute("audioVersion", "2"); //? 28 | doc.appendChild(root); 29 | 30 | // Some helpful comments 31 | QDomComment artistComment = doc.createComment(QString("Artist: ") + s.artist); 32 | QDomComment titleComment = doc.createComment(QString("Title: ") + s.title); 33 | root.appendChild(artistComment); 34 | root.appendChild(titleComment); 35 | 36 | // Track element 37 | int tracknum = 1; 38 | QDomElement trackElem = doc.createElement("TRACK"); 39 | trackElem.setAttribute("Name", "Player1"); 40 | trackElem.setAttribute("Artist", s.artist); 41 | root.appendChild(trackElem); 42 | 43 | // Create first sentence 44 | int sentencenum = 1; 45 | QDomElement sentenceElem = doc.createElement("SENTENCE"); // FIXME: Should there be Singer and Part attributes? 46 | QDomComment sentenceComment = doc.createComment(QString("Track %1, Sentence %2").arg(tracknum).arg(sentencenum)); 47 | sentenceElem.appendChild(sentenceComment); 48 | // First sentence also needs a starting SLEEP 49 | Notes const& notes = s.getVocalTrack().notes; 50 | int ts = sec2dur(notes.front().begin); 51 | QDomElement firstNoteElem = doc.createElement("NOTE"); 52 | firstNoteElem.setAttribute("MidiNote", "0"); 53 | firstNoteElem.setAttribute("Duration", QString::number(ts)); 54 | firstNoteElem.setAttribute("Lyric", ""); 55 | sentenceElem.appendChild(firstNoteElem); 56 | bool firstNote = true; 57 | 58 | // Iterate all notes 59 | for (unsigned int i = 0; i < notes.size(); ++i) { 60 | Note const& n = notes[i]; 61 | if (n.type == Note::SLEEP) continue; // Skip SLEEPs 62 | 63 | // New sentence 64 | if (n.lineBreak && !firstNote) { 65 | root.appendChild(sentenceElem); 66 | ++sentencenum; 67 | sentenceElem = doc.createElement("SENTENCE"); 68 | sentenceComment = doc.createComment(QString("Track %1, Sentence %2").arg(tracknum).arg(sentencenum)); 69 | sentenceElem.appendChild(sentenceComment); 70 | } 71 | firstNote = false; 72 | 73 | // Construct a regular note element 74 | int l = sec2dur(n.length()); ts += l; 75 | QDomElement noteElem = doc.createElement("NOTE"); 76 | noteElem.setAttribute("MidiNote", QString::number(n.note)); 77 | noteElem.setAttribute("Duration", QString::number(l)); 78 | noteElem.setAttribute("Lyric", n.syllable); 79 | if (n.type == Note::GOLDEN) noteElem.setAttribute("Bonus", "Yes"); 80 | if (n.type == Note::FREESTYLE) noteElem.setAttribute("FreeStyle", "Yes"); 81 | sentenceElem.appendChild(noteElem); 82 | 83 | // Construct a note element, indicationg the pause before next note 84 | // This is only done if the pause has duration 85 | // We also take the overall position into consideration (counter rounding errors) 86 | int pauseLen = 0; 87 | double end = dur2sec(ts); 88 | for (int j = i + 1; j < notes.size(); ++j) { // Find the next non-SLEEP note 89 | if (notes[j].type != Note::SLEEP) { 90 | pauseLen = sec2dur(notes[j].begin - end); // Difference to next note 91 | break; 92 | } 93 | } 94 | if (pauseLen > 0) { 95 | QDomElement pauseElem = doc.createElement("NOTE"); 96 | pauseElem.setAttribute("MidiNote", "0"); 97 | pauseElem.setAttribute("Duration", QString::number(pauseLen)); 98 | pauseElem.setAttribute("Lyric", ""); 99 | sentenceElem.appendChild(pauseElem); 100 | ts += pauseLen; 101 | } 102 | } 103 | root.appendChild(sentenceElem); 104 | 105 | // Get the xml data 106 | QString xml = doc.toString(4); 107 | // Write to file 108 | QFile f(path + "/notes.xml"); 109 | if (f.open(QFile::WriteOnly)) { 110 | QTextStream out(&f); 111 | out.setCodec("UTF-8"); 112 | out << xml; 113 | } else throw std::runtime_error("Couldn't open target file notes.xml"); 114 | } 115 | 116 | int SingStarXMLWriter::sec2dur(double sec) const { 117 | return round(tempo / 60.0 * sec * (res == "Demisemiquaver" ? 8 : 4)); 118 | } 119 | 120 | double SingStarXMLWriter::dur2sec(int ts) const { 121 | return ts * 60.0 / (tempo * (res == "Demisemiquaver" ? 8 : 4)); 122 | } 123 | -------------------------------------------------------------------------------- /src/editorapp.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ui_editor.h" 4 | #include "ui_aboutdialog.h" 5 | #include "operation.hh" 6 | #include "song.hh" 7 | #include "synth.hh" 8 | #include "scrollbar.hh" 9 | #include "notegraphwidget.hh" 10 | #include 11 | 12 | class QProgressBar; 13 | class QPushButton; 14 | class QCloseEvent; 15 | class NoteLabel; 16 | class NoteGraphWidget; 17 | class GettingStartedDialog; 18 | 19 | 20 | class AboutDialog: public QDialog, private Ui::AboutDialog 21 | { 22 | Q_OBJECT 23 | public: 24 | AboutDialog(QWidget* parent = 0); 25 | }; 26 | 27 | 28 | class Piano: public QLabel 29 | { 30 | Q_OBJECT 31 | public: 32 | Piano(QWidget *parent = 0); 33 | public slots: 34 | void updatePixmap(NoteGraphWidget *ngw); 35 | protected: 36 | void mousePressEvent(QMouseEvent *event); 37 | void mouseMoveEvent(QMouseEvent *event); 38 | private: 39 | BufferPlayer *m_player; 40 | }; 41 | 42 | 43 | class EditorApp: public QMainWindow 44 | { 45 | Q_OBJECT 46 | 47 | public: 48 | EditorApp(QWidget *parent = 0); 49 | 50 | void openFile(QString fileName); 51 | void updateSongMeta(bool readFromSongToUI = false); 52 | void updateMenuStates(); 53 | void updateTitle(); 54 | void highlightLabel(QString id); 55 | void showExportMenu() { ui.menuExport->exec(pos() + QPoint(0, ui.menubar->height())); } 56 | 57 | private: 58 | void setupNoteGraph(); 59 | void setMusic(QString filepath, bool primary = true); 60 | void setVideo(QString filepath); 61 | void setBPM(double); 62 | bool promptSaving(); 63 | void saveProject(QString fileName); 64 | void exportSong(QString format, QString dialogTitle); 65 | void doOpStack(); 66 | void playButton(); 67 | void readSettings(); 68 | void writeSettings(); 69 | 70 | public slots: 71 | void operationDone(const Operation &op); 72 | void updateNoteInfo(NoteLabel *note); 73 | void analyzeProgress(int value, int maximum); 74 | void metaDataChanged(); 75 | void audioTick(qint64 time); 76 | void playerStateChanged(QMediaPlayer::State state); 77 | void playerError(QMediaPlayer::Error error); 78 | void playbackRateChanged(qreal rate); 79 | void playBuffer(const QByteArray& buffer); 80 | void statusBarMessage(const QString& message); 81 | void updatePiano(int y); 82 | void clearLabelHighlights(); 83 | 84 | // Automatic slots 85 | 86 | void on_cmdPlay_clicked(); 87 | void on_chkSynth_clicked(bool checked); 88 | 89 | // File menu 90 | void on_actionNew_triggered(); 91 | void on_actionOpen_triggered(); 92 | void on_actionSave_triggered(); 93 | void on_actionSaveAs_triggered(); 94 | void on_actionSingStarXML_triggered(); 95 | void on_actionUltraStarTXT_triggered(); 96 | void on_actionFoFMIDI_triggered(); 97 | void on_actionLRC_triggered(); 98 | void on_actionEnhanced_LRC_triggered(); 99 | void on_actionSoramimiTXT_triggered(); 100 | void on_actionLyricsToFile_triggered(); 101 | void on_actionLyricsToClipboard_triggered(); 102 | void on_actionExit_triggered(); 103 | 104 | // Edit menu 105 | void on_actionUndo_triggered(); 106 | void on_actionRedo_triggered(); 107 | void on_actionDelete_triggered(); 108 | void on_actionSelectAll_triggered(); 109 | void on_actionSelectAllAfter_triggered(); 110 | void on_actionAntiAliasing_toggled(bool checked); 111 | 112 | // Insert menu 113 | void on_actionMusicFile_triggered(); 114 | void on_actionVideoFile_triggered(); 115 | void on_actionAdditionalMusicFile_triggered(); 116 | void on_actionLyricsFromFile_triggered(); 117 | void on_actionLyricsFromClipboard_triggered(); 118 | void on_actionLyricsFromLRCFile_triggered(); 119 | 120 | // View menu 121 | void on_actionZoomIn_triggered(); 122 | void on_actionZoomOut_triggered(); 123 | void on_actionResetZoom_triggered(); 124 | 125 | // Help menu 126 | void on_actionGettingStarted_triggered(); 127 | void on_actionWhatsThis_triggered(); 128 | void on_actionAbout_triggered(); 129 | void on_actionAboutQt_triggered(); 130 | 131 | // Note properties tab 132 | void on_txtTitle_editingFinished(); 133 | void on_txtArtist_editingFinished(); 134 | void on_txtGenre_editingFinished(); 135 | void on_txtYear_editingFinished(); 136 | void on_txtBPM_editingFinished(); 137 | void on_cmdSplit_clicked(); 138 | void on_cmdInsert_clicked(); 139 | void on_cmbNoteType_activated(int state); 140 | void on_chkFloating_clicked(bool checked); 141 | void on_chkLineBreak_clicked(bool checked); 142 | void on_sliderPlaybackRate_valueChanged(int value); 143 | 144 | protected: 145 | void closeEvent(QCloseEvent *event); 146 | 147 | private slots: 148 | void updatedNotes(); 149 | 150 | private: 151 | Ui::EditorApp ui; 152 | GettingStartedDialog *gettingStarted; 153 | NoteGraphWidget *noteGraph; 154 | OperationStack opStack; 155 | OperationStack redoStack; 156 | QScopedPointer song; 157 | QMediaPlayer *player; 158 | BufferPlayer *bufferPlayers[2]; 159 | QScopedPointer synth; 160 | Piano *piano; 161 | QProgressBar *statusbarProgress; 162 | QPushButton *statusbarButton; 163 | QString projectFileName; 164 | QString latestPath; 165 | int currentBufferPlayer; 166 | ScrollBar* m_scrollBar = nullptr; 167 | bool m_scrollBarNeedUpdate = true; 168 | }; 169 | -------------------------------------------------------------------------------- /ui/aboutdialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | AboutDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 531 10 | 320 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 | 21 | &About 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 29 | 75 30 | true 31 | 32 | 33 | 34 | ApplicationName 35 | 36 | 37 | Qt::AlignCenter 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 16 46 | 47 | 48 | 49 | Version: XY 50 | 51 | 52 | Qt::AlignCenter 53 | 54 | 55 | 56 | 57 | 58 | 59 | Qt::Vertical 60 | 61 | 62 | 63 | 20 64 | 40 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | Website: <a href="http://performous.org/composer">http://performous.org/composer</a> 73 | 74 | 75 | Qt::RichText 76 | 77 | 78 | Qt::AlignCenter 79 | 80 | 81 | true 82 | 83 | 84 | 85 | 86 | 87 | 88 | Qt::Vertical 89 | 90 | 91 | 92 | 20 93 | 40 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | This is an application for creating notes for karaoke games that score the performance based on pitch accuracy. 102 | 103 | 104 | true 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | A&uthors 113 | 114 | 115 | 116 | 117 | 118 | QPlainTextEdit::NoWrap 119 | 120 | 121 | true 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | &License 130 | 131 | 132 | 133 | 134 | 135 | QPlainTextEdit::NoWrap 136 | 137 | 138 | true 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | &Close 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /.github/workflows/build_and_release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release Composer 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Run on a schedule to get monthly updates 6 | schedule: 7 | - cron: "0 0 28 * *" 8 | 9 | # Triggers the workflow on merges to master, release branches, 10 | # all PRs, and release tags 11 | push: 12 | branches: 13 | - master 14 | - '[0-9]+\.[0-9]+\.[0-9]+-rc[0-9]+' 15 | tags: 16 | - '[0-9]+\.[0-9]+\.[0-9]+' 17 | - '[0-9]+\.[0-9]+\.[0-9]+-rc[0-9]+' 18 | 19 | # On anything pull request related 20 | pull_request: 21 | 22 | # Allows you to run this workflow manually from the Actions tab 23 | workflow_dispatch: 24 | 25 | # Note: entire jobs or sections can be disabled by adding 26 | # if: ${{ false }} to the definition column 27 | jobs: 28 | # Determine version 29 | determine_version: 30 | name: Determine the version to be used 31 | runs-on: ubuntu-latest 32 | outputs: 33 | latest_tag_version: ${{ steps.versioning.outputs.latest_tag_version }} 34 | latest_full_tag_version: ${{ steps.versioning.outputs.latest_full_tag_version }} 35 | version_major: ${{ steps.versioning.outputs.version_major }} 36 | version_minor: ${{ steps.versioning.outputs.version_minor }} 37 | version_patch: ${{ steps.versioning.outputs.version_patch }} 38 | version_tweak: ${{ steps.versioning.outputs.version_tweak }} 39 | complete_version: ${{ steps.versioning.outputs.complete_version }} 40 | steps: 41 | - name: Determine the complete version 42 | id: versioning 43 | run: | 44 | # Always check the tags on master since it will have the latest. 45 | # Tags will trigger their own workflow and version names 46 | git clone --recursive ${{ github.server_url }}/${{ github.repository }} performous_composer 47 | cd performous_composer 48 | LATEST_TAG_VERSION=$(git describe --tags --abbrev=0 || echo 1.0.0) 49 | LATEST_FULL_TAG_VERSION=$(git describe --tags || echo 1.0.0) 50 | echo "latest_tag_version=$(git describe --tags --abbrev=0 || echo 1.0.0)" >> $GITHUB_OUTPUT 51 | echo "latest_full_tag_version=$(git describe --tags || echo 1.0.0)" >> $GITHUB_OUTPUT 52 | echo "version_major=$(cut -d '.' -f 1 <<< $(git describe --tags --abbrev=0 || echo 1.0.0))" >> $GITHUB_OUTPUT 53 | echo "version_minor=$(cut -d '.' -f 2 <<< $(git describe --tags --abbrev=0 || echo 1.0.0))" >> $GITHUB_OUTPUT 54 | echo "version_patch=$(cut -d '.' -f 3 <<< $(git describe --tags --abbrev=0 || echo 1.0.0))" >> $GITHUB_OUTPUT 55 | echo "version_tweak=0" >> $GITHUB_OUTPUT 56 | echo "complete_version=$(if [ $GITHUB_REF_TYPE = 'tag' ]; then echo $GITHUB_REF_NAME; elif [ $GITHUB_REF_TYPE = 'branch' ] && [ $GITHUB_REF_NAME = 'master' ]; then echo $LATEST_FULL_TAG_VERSION-beta; elif [ $GITHUB_REF_TYPE = 'branch' ] && [ $GITHUB_REF_NAME != 'master' ]; then echo $LATEST_TAG_VERSION-${{github.event.pull_request.number}}-${GITHUB_SHA::7}-alpha; fi)" >> $GITHUB_OUTPUT 57 | 58 | # Set up a release that packages will be published to. 59 | create_release: 60 | name: Create a release 61 | runs-on: ubuntu-latest 62 | # Make sure the output variable for this step is set so it 63 | # can be consumed by later build steps 64 | outputs: 65 | upload_url: ${{ steps.create_release.outputs.upload_url }} 66 | steps: 67 | - name: Create the Main release 68 | id: create_release 69 | if: ${{ github.event_name != 'pull_request' && github.ref_type == 'tag' }} 70 | uses: actions/create-release@v1 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | with: 74 | tag_name: ${{ github.ref_name }} 75 | release_name: Composer ${{ github.ref_name }} 76 | draft: true 77 | prerelease: false 78 | 79 | # Pull in the Linux build workflow 80 | Linux_Packages: 81 | name: Build the Linux packages 82 | uses: ./.github/workflows/linux.yml 83 | with: 84 | package_complete_version: ${{ needs.determine_version.outputs.complete_version }} 85 | release_upload_url: ${{ needs.create_release.outputs.upload_url }} 86 | needs: 87 | - determine_version 88 | - create_release 89 | 90 | # Pull in the AppImage build workflow 91 | AppImage_Package: 92 | name: Build the AppImage 93 | uses: ./.github/workflows/appimage.yml 94 | with: 95 | package_complete_version: ${{ needs.determine_version.outputs.complete_version }} 96 | release_upload_url: ${{ needs.create_release.outputs.upload_url }} 97 | needs: 98 | - determine_version 99 | - create_release 100 | 101 | # Pull in the MacOS build workflow 102 | #MacOS_Package: 103 | # name: Build the MacOS package 104 | # uses: ./.github/workflows/macos.yml 105 | # with: 106 | # package_complete_version: ${{ needs.determine_version.outputs.complete_version }} 107 | # release_upload_url: ${{ needs.create_release.outputs.upload_url }} 108 | # needs: 109 | # - determine_version 110 | # - create_release 111 | 112 | # Pull in the Windows build workflow 113 | #Windows_Packages: 114 | # name: Build the Windows packages 115 | # uses: ./.github/workflows/windows.yml 116 | # with: 117 | # package_complete_version: ${{ needs.determine_version.outputs.complete_version }} 118 | # release_upload_url: ${{ needs.create_release.outputs.upload_url }} 119 | # needs: 120 | # - determine_version 121 | # - create_release 122 | -------------------------------------------------------------------------------- /src/songparser-txt.cc: -------------------------------------------------------------------------------- 1 | #include "songparser.hh" 2 | 3 | #include 4 | #include 5 | 6 | /// @file 7 | /// Functions used for parsing the UltraStar TXT song format 8 | 9 | using namespace SongParserUtil; 10 | 11 | /// 'Magick' to check if this file looks like correct format 12 | bool SongParser::txtCheck(QString const& data) { 13 | return data[0] == '#' && data[1] >= 'A' && data[1] <= 'Z'; 14 | } 15 | 16 | /// Parser 17 | void SongParser::txtParse() { 18 | QString line; 19 | // Parse header 20 | while (getline(line) && txtParseField(line)) {} 21 | if (m_song.title.isEmpty() || m_song.artist.isEmpty()) 22 | throw std::runtime_error("Required header fields missing"); 23 | if (m_song.bpm != 0.0) addBPM(0, m_song.bpm); 24 | 25 | // Parse notes 26 | VocalTrack vocal(TrackName::LEAD_VOCAL); 27 | while (txtParseNote(line, vocal) && getline(line)) {} 28 | 29 | // Workaround for the terminating : 1 0 0 line, written by some converters 30 | if (!vocal.notes.empty() && vocal.notes.back().type != Note::SLEEP 31 | && vocal.notes.back().begin == vocal.notes.back().end) vocal.notes.pop_back(); 32 | 33 | m_song.insertVocalTrack(TrackName::LEAD_VOCAL, vocal); 34 | } 35 | 36 | bool SongParser::txtParseField(QString const& line) { 37 | if (line.isEmpty()) return true; 38 | if (line[0] != '#') return false; 39 | int pos = line.indexOf(':'); 40 | if (pos < 0) throw std::runtime_error("Invalid txt format, should be #key:value"); 41 | QString key = line.left(pos).trimmed().mid(1); 42 | QString value = line.mid(pos + 1).trimmed(); 43 | bool ok = true; 44 | if (value.isEmpty()) return true; 45 | if (key == "TITLE") m_song.title = value; 46 | else if (key == "ARTIST") m_song.artist = value; 47 | else if (key == "EDITION") m_song.edition = value; 48 | else if (key == "GENRE") m_song.genre = value; 49 | else if (key == "CREATOR") m_song.creator = value; 50 | else if (key == "COVER") m_song.cover = value; 51 | else if (key == "MP3") m_song.music["background"] = m_song.path + value; 52 | else if (key == "VOCALS") m_song.music["vocals"] = m_song.path + value; 53 | else if (key == "VIDEO") m_song.video = value; 54 | else if (key == "BACKGROUND") m_song.background = value; 55 | else if (key == "START") m_song.start = value.replace(',','.').toDouble(&ok); 56 | else if (key == "VIDEOGAP") m_song.videoGap = value.replace(',','.').toDouble(&ok); 57 | else if (key == "PREVIEWSTART") m_song.preview_start = value.replace(',','.').toDouble(&ok); 58 | else if (key == "RELATIVE") assign(m_relative, value); 59 | else if (key == "GAP") { m_gap = value.replace(',','.').toDouble(&ok); m_gap *= 1e-3; } 60 | else if (key == "BPM") m_song.bpm = value.replace(',','.').toDouble(&ok); 61 | else if (key == "LANGUAGE") m_song.language = value; 62 | else if (key == "YEAR") m_song.year = value; 63 | 64 | if (!ok) throw std::runtime_error(QString("Invalid value for %1: %2").arg(key).arg(value).toStdString()); 65 | return true; 66 | } 67 | 68 | bool SongParser::txtParseNote(QString line, VocalTrack &vocal) { 69 | if (line.isEmpty() || line == "\r") return true; 70 | // Trim leading whitespace (after is preserved for word breaking purposes) 71 | while (line[0].isSpace()) { 72 | line = line.mid(1); 73 | if (line.isEmpty()) return true; 74 | } 75 | if (line[0] == '#') throw std::runtime_error("Key found in the middle of notes"); 76 | if (line[0] == 'E') return false; 77 | QTextStream iss(&line); 78 | if (line[0] == 'B') { 79 | unsigned int ts; 80 | double bpm; 81 | QChar ignore; iss >> ignore; 82 | iss >> ts >> bpm; 83 | if (iss.status() != QTextStream::Ok) throw std::runtime_error("Invalid BPM line format"); 84 | addBPM(ts, bpm); 85 | return true; 86 | } 87 | if (line[0] == 'P') return true; //We ignore player information for now (multiplayer hack) 88 | Note n; 89 | n.type = Note::Type(iss.read(1)[0].toLatin1()); 90 | unsigned int ts = m_prevts; 91 | switch (n.type) { 92 | case Note::NORMAL: 93 | case Note::FREESTYLE: 94 | case Note::GOLDEN: 95 | { 96 | unsigned int length = 0; 97 | iss >> ts >> length >> n.note; 98 | if (iss.status() != QTextStream::Ok) throw std::runtime_error("Invalid note line format"); 99 | n.notePrev = n.note; // No slide notes in TXT yet. 100 | if (m_relative) ts += m_relativeShift; 101 | if (iss.read(1)[0].toLatin1() == ' ') n.syllable = iss.readLine(); 102 | n.end = tsTime(ts + length); 103 | } 104 | break; 105 | case Note::SLEEP: 106 | { 107 | unsigned int end; 108 | iss >> ts >> end; 109 | if (iss.status() != QTextStream::Ok) end = ts; 110 | if (m_relative) { 111 | ts += m_relativeShift; 112 | end += m_relativeShift; 113 | m_relativeShift = end; 114 | } 115 | n.end = tsTime(end); 116 | } 117 | break; 118 | default: throw std::runtime_error("Unknown note type"); 119 | } 120 | n.begin = tsTime(ts); 121 | Notes& notes = vocal.notes; 122 | if (m_relative && notes.empty()) m_relativeShift = ts; 123 | m_prevts = ts; 124 | if (n.begin < m_prevtime) { 125 | // Oh no, overlapping notes (b0rked file) 126 | // Can't do this because too many songs are b0rked: throw std::runtime_error("Note overlaps with previous note"); 127 | if (notes.size() >= 1) { 128 | Note& p = notes.back(); 129 | // Workaround for songs that use semi-random timestamps for sleep 130 | if (p.type == Note::SLEEP) { 131 | p.end = p.begin; 132 | Notes::reverse_iterator it = notes.rbegin(); 133 | Note& p2 = *++it; 134 | if (p2.end < n.begin) p.begin = p.end = n.begin; 135 | } 136 | // Can we just make the previous note shorter? 137 | if (p.begin <= n.begin) p.end = n.begin; 138 | else { // Nothing to do, warn and skip 139 | std::cout << "Skipping overlapping note in " << m_song.path.toStdString() << m_song.filename.toStdString() << std::endl; 140 | return true; 141 | } 142 | } else throw std::runtime_error("The first note has negative timestamp"); 143 | } 144 | double prevtime = m_prevtime; 145 | m_prevtime = n.end; 146 | if (n.type != Note::SLEEP && n.end > n.begin) { 147 | vocal.noteMin = std::min(vocal.noteMin, n.note); 148 | vocal.noteMax = std::max(vocal.noteMax, n.note); 149 | } 150 | if (n.type == Note::SLEEP) { 151 | if (notes.empty()) return true; // Ignore sleeps at song beginning 152 | n.begin = n.end = prevtime; // Normalize sleep notes 153 | } 154 | notes.push_back(n); 155 | return true; 156 | } 157 | 158 | -------------------------------------------------------------------------------- /src/songparser.cc: -------------------------------------------------------------------------------- 1 | #include "songparser.hh" 2 | #include "textcodecselector.hh" 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | namespace SongParserUtil { 9 | void assign(bool& var, QString const& str) { 10 | if (str == "YES" || str == "yes" || str == "1") var = true; 11 | else if (str == "NO" || str == "no" || str == "0") var = false; 12 | else throw std::runtime_error("Invalid boolean value: " + str.toStdString()); 13 | } 14 | } 15 | 16 | 17 | /// constructor 18 | SongParser::SongParser(Song& s): 19 | m_song(s), 20 | m_stream(), 21 | m_linenum(), 22 | m_relative(), 23 | m_gap(), 24 | m_prevtime(), 25 | m_prevts(), 26 | m_relativeShift(), 27 | m_maxScore(), 28 | m_tsPerBeat(), 29 | m_tsEnd() 30 | { 31 | enum { NONE, TXT, XML, INI, MIDI, SM, LRC } type = NONE; 32 | // Read the file, determine the type and do some initial validation checks 33 | QFile file(m_song.path + m_song.filename); 34 | if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) 35 | throw SongParserException(QT_TR_NOOP("Could not open song file"), 0); 36 | 37 | QFileInfo finfo(file); 38 | if (finfo.size() < 10 || finfo.size() > 100000) throw SongParserException("Does not look like a song file (wrong size)", 1, true); 39 | 40 | // Determine encoding 41 | QString data = TextCodecSelector::readAllAndHandleEncoding(file); 42 | file.close(); 43 | // Add a newline to the end to make sure our parsing doesn't skip the last line 44 | data += "\n"; 45 | 46 | if (smCheck(data)) type = SM; 47 | else if (txtCheck(data)) type = TXT; 48 | else if (xmlCheck(data)) type = XML; 49 | else if (iniCheck(data)) type = INI; 50 | else if (midiCheck(data)) type = MIDI; 51 | else if (lrcCheck(data)) type = LRC; 52 | else throw SongParserException("Does not look like a song file (wrong header)", 1, true); 53 | 54 | m_stream.setString(&data); 55 | 56 | // Parse 57 | try { 58 | if (type == TXT) txtParse(); 59 | else if (type == XML) xmlParse(); 60 | else if (type == INI) iniParse(); 61 | else if (type == MIDI) midParse(); 62 | else if (type == SM) smParse(); 63 | else if (type == LRC) lrcParse(); 64 | } catch (std::runtime_error& e) { 65 | throw SongParserException(e.what(), m_linenum); 66 | } 67 | 68 | // Remove bogus entries 69 | if (!QFileInfo(m_song.path + m_song.cover).exists()) m_song.cover = ""; 70 | if (!QFileInfo(m_song.path + m_song.background).exists()) m_song.background = ""; 71 | if (!QFileInfo(m_song.path + m_song.video).exists()) m_song.video = ""; 72 | 73 | // TODO: Should we try to guess images and stuff? 74 | 75 | finalize(); // Do some adjusting to the notes 76 | s.loadStatus = Song::FULL; 77 | } 78 | 79 | bool SongParser::getline(QString &line) 80 | { 81 | ++m_linenum; 82 | line = m_stream.readLine(); 83 | return !m_stream.atEnd(); 84 | } 85 | 86 | namespace { 87 | /// Return the amount of shift (in notes) required for note to make put it in the nearest octave of the target note 88 | int nearestOctave(int note, int target) { 89 | return (1200 + 6 + target - note) / 12 * 12 - 1200; // 1200 for always positive, 6 for mathematical rounding 90 | } 91 | void normalize(Notes& notes, int limLow, int limHigh) { 92 | // Analyze the entire song first 93 | int defaultShift = 0; 94 | int shiftFS = 0; 95 | { 96 | std::vector fsNotes, regNotes; 97 | for (Notes::iterator it = notes.begin(); it != notes.end(); ++it) { 98 | (it->type == Note::FREESTYLE ? fsNotes : regNotes).push_back(it->note); 99 | } 100 | if (!regNotes.empty()) { 101 | std::sort(regNotes.begin(), regNotes.end()); 102 | // Find a good starting value for default shift 103 | defaultShift = nearestOctave(regNotes[regNotes.size() / 2], 0.5 * (limLow + limHigh)); 104 | // Find the additional correction required for freestyle notes 105 | if (!fsNotes.empty()) { 106 | std::sort(fsNotes.begin(), fsNotes.end()); 107 | shiftFS = nearestOctave(fsNotes[fsNotes.size() / 2], regNotes[regNotes.size() / 2]); 108 | } 109 | } 110 | } 111 | // Process sentence by sentence 112 | for (Notes::iterator it = notes.begin(), itnext = it; it != notes.end();) { 113 | int low, high; 114 | low = high = it->note; 115 | // Analyze the sentence and find the end of it 116 | while (++itnext != notes.end() && !itnext->lineBreak) { 117 | if (itnext->type == Note::SLEEP) continue; 118 | int n = itnext->note; 119 | if (itnext->type == Note::FREESTYLE) n += shiftFS; 120 | low = std::min(low, n); 121 | high = std::max(high, n); 122 | } 123 | // Per-sentence shift 124 | int shift = nearestOctave(0.5 * (low + high), 0.5 * (limLow + limHigh)); 125 | if (std::abs(defaultShift - shift) <= 12) shift = defaultShift; 126 | // Shift the notes into position 127 | for (; it != itnext; ++it) { 128 | if (it->type == Note::SLEEP) continue; 129 | int s = shift; 130 | if (it->type == Note::FREESTYLE) s += shiftFS; 131 | // The last resort if everything else fails (per-note shifting) 132 | while (it->note + s < limLow) s += 12; 133 | while (it->note + s > limHigh) s -= 12; 134 | it->note += s; 135 | it->notePrev += s; 136 | } 137 | } 138 | } 139 | } 140 | 141 | void SongParser::finalize() { 142 | std::vector tracks = m_song.getVocalTrackNames(); 143 | for(std::vector::const_iterator it = tracks.begin() ; it != tracks.end() ; ++it) { 144 | VocalTrack& vocal = m_song.getVocalTrack(*it); 145 | vocal.m_scoreFactor = 1.0 / m_maxScore; 146 | if (vocal.notes.empty()) continue; 147 | // Set begin/end times 148 | vocal.beginTime = vocal.notes.front().begin, vocal.endTime = vocal.notes.back().end; 149 | // Setup sentence start indicators and remove sleep notes 150 | bool sentenceStart = true; 151 | for (Notes::iterator it = vocal.notes.begin(); it != vocal.notes.end();) { 152 | if (sentenceStart) it->lineBreak = true; 153 | sentenceStart = false; 154 | if (it->type == Note::SLEEP) { 155 | sentenceStart = true; 156 | it = vocal.notes.erase(it); 157 | } else ++it; 158 | } 159 | // Note normalization 160 | const int limLow = 1, limHigh = 47; 161 | if (vocal.noteMin < limLow || vocal.noteMax > limHigh) normalize(vocal.notes, limLow, limHigh); 162 | } 163 | if (m_tsPerBeat) { 164 | // Add song beat markers 165 | for (unsigned ts = 0; ts < m_tsEnd; ts += m_tsPerBeat) m_song.beats.push_back(tsTime(ts)); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/notegraphwidget.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pitchvis.hh" 4 | #include "notes.hh" 5 | #include "operation.hh" 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | class QScrollArea; 12 | class NoteLabel; 13 | typedef QList NoteLabels; 14 | 15 | 16 | class SeekHandle: public QLabel 17 | { 18 | Q_OBJECT 19 | public: 20 | SeekHandle(QWidget *parent = 0); 21 | int curx() const { return x() + width() / 2; } 22 | bool wrapToViewport; 23 | protected: 24 | void mouseMoveEvent(QMouseEvent *event); 25 | void moveEvent(QMoveEvent *event); 26 | }; 27 | 28 | 29 | 30 | class NoteLabelManager: public QLabel 31 | { 32 | Q_OBJECT 33 | public: 34 | static const QString MimeType; 35 | 36 | NoteLabelManager(QWidget *parent = 0); 37 | 38 | virtual void updateNotes(bool leftToRight = true) {} 39 | virtual void startNotePixmapUpdates() {} 40 | virtual void forcedNotePixmapUpdate() {} 41 | 42 | void reset(); 43 | void clearNotes(); 44 | void selectNote(NoteLabel *note, bool clearPrevious = true); 45 | void selectAll(); 46 | void selectAllAfter(); 47 | void shiftSelect(NoteLabel *note); 48 | void boxSelect(QPoint p1, QPoint p2); 49 | NoteLabel* selectedNote() const { return m_selectedNotes.isEmpty() ? NULL : m_selectedNotes.front(); } 50 | NoteLabels& selectedNotes() { return m_selectedNotes; } 51 | NoteLabels const& selectedNotes() const { return m_selectedNotes; } 52 | 53 | int getNoteLabelId(NoteLabel* note) const; 54 | int findIdForTime(double time) const; 55 | NoteLabels& noteLabels() { return m_notes; } 56 | 57 | void createNote(double time); 58 | void split(NoteLabel *note, float ratio = 0.5f); 59 | void del(NoteLabel *note); 60 | void move(NoteLabel *note, int value); 61 | void editLyric(NoteLabel *note); 62 | void setFloating(NoteLabel *note, bool state); 63 | void setLineBreak(NoteLabel *note, bool state); 64 | void setType(NoteLabel *note, int newtype); 65 | 66 | void doOperation(const Operation& op, int flags = Operation::NORMAL); 67 | 68 | virtual void zoom(float steps, double focalSecs = -1); 69 | int getZoomLevel() const; 70 | 71 | int s2px(double sec) const; 72 | double px2s(int px) const; 73 | int n2px(double note) const; 74 | double px2n(int px) const; 75 | 76 | double getSongLengthInSeconds() const; 77 | 78 | signals: 79 | void updateNoteInfo(NoteLabel*); 80 | void operationDone(const Operation&); 81 | void statusBarMessage(QString); 82 | 83 | public slots: 84 | void selectNextSyllable(bool backwards = false, bool addToSelection = false); 85 | void selectNextSentenceStart(); 86 | void cut(); 87 | void copy(); 88 | void paste(); 89 | 90 | protected: 91 | QScrollArea* getScrollArea() const; 92 | void calcViewport(int &x1, int &y1, int &x2, int &y2) const; 93 | 94 | // Zoom settings 95 | static const double zoomStep; ///< Mouse wheel steps * zoomStep => double/half zoom factor 96 | static const int zoomMin = -12; ///< Number of steps to minimum zoom 97 | static const int zoomMax = 6; ///< Number of steps to maximum zoom 98 | static const double ppsNormal; ///< Pixels per second with default zoom 99 | double m_pixelsPerSecond; 100 | 101 | NoteLabels m_notes; 102 | NoteLabels m_selectedNotes; 103 | enum NoteAction { NONE, RESIZE, MOVE } m_selectedAction; 104 | int m_noteHalfHeight; 105 | double m_songLengthInSeconds; 106 | }; 107 | 108 | 109 | const int MaxPitchVis = 2; 110 | 111 | class NoteGraphWidget: public NoteLabelManager 112 | { 113 | Q_OBJECT 114 | 115 | public: 116 | static const QString BGColor; 117 | static const int Height; 118 | 119 | NoteGraphWidget(QWidget *parent = 0); 120 | 121 | void setLyrics(QString lyrics); 122 | void setLyrics(const VocalTrack &track); 123 | void analyzeMusic(QString filepath, int visId = 0); 124 | 125 | void updateNotes(bool leftToRight = true); 126 | void updateMusicPos(qint64 time, bool smoothing = true); 127 | void stopMusic(); 128 | void seek(int x); 129 | void zoom(float steps, double focalSecs = -1); 130 | 131 | VocalTrack getVocalTrack() const; 132 | QString getCurrentSentence() const; 133 | QString getPrevSentence() const; 134 | QString dumpLyrics() const; 135 | 136 | public slots: 137 | void showContextMenu(const QPoint &pos); 138 | void timeSyllable(); 139 | void timeSentence(); 140 | void setSeekHandleWrapToViewport(bool state) { m_seekHandle.wrapToViewport = state; } 141 | void updatePixmap(const QImage &image, const QPoint &position, int visId); 142 | void updatePitch(); 143 | void abortPitch() { for (int i = 0; i < MaxPitchVis; ++i) if (m_pitch[i]) m_pitch[i]->cancel(); } 144 | void scrollToFirstNote(); 145 | void startNotePixmapUpdates(); ///< Starts creating pixmaps for NoteLabels 146 | void forcedNotePixmapUpdate(); 147 | void playbackRateChanged(qreal rate); 148 | 149 | signals: 150 | void analyzeProgress(int, int); 151 | void seeked(qint64 time); 152 | void updatedNotes(); 153 | 154 | protected: 155 | void mousePressEvent(QMouseEvent *event); 156 | void mouseReleaseEvent(QMouseEvent *event); 157 | void wheelEvent(QWheelEvent *event); 158 | void mouseDoubleClickEvent(QMouseEvent *event); 159 | void mouseMoveEvent(QMouseEvent * event); 160 | void keyPressEvent(QKeyEvent *event); 161 | void timerEvent(QTimerEvent *event); 162 | void paintEvent(QPaintEvent*); 163 | void resizeEvent(QResizeEvent *) { updatePitch(); } 164 | void dragEnterEvent(QDragEnterEvent *event); 165 | void dropEvent(QDropEvent *event); 166 | 167 | private: 168 | void finalizeNewLyrics(); 169 | void timeCurrent(); 170 | 171 | QPoint m_mouseHotSpot; 172 | bool m_seeking; 173 | bool m_actionHappened; 174 | QScopedPointer m_pitch[MaxPitchVis]; 175 | SeekHandle m_seekHandle; 176 | int m_nextNotePixmap; 177 | int m_notePixmapTimer; 178 | int m_analyzeTimer; 179 | int m_playbackTimer; 180 | QElapsedTimer m_playbackInterval; 181 | qint64 m_playbackPos; 182 | qreal m_playbackRate; 183 | QPixmap m_pixmap[MaxPitchVis]; 184 | QPoint m_pixmapPos[MaxPitchVis]; 185 | }; 186 | 187 | 188 | struct FloatingGap 189 | { 190 | FloatingGap(double startTime): begin(startTime), end(startTime), m_notesLength() {} 191 | 192 | void addNote(NoteLabel* n); 193 | bool isEmpty() const { return notes.empty(); } 194 | double length() const { return std::abs(end - begin); } 195 | double minLength() const; 196 | double notesLength() const { return m_notesLength; } 197 | 198 | double begin; 199 | double end; 200 | 201 | NoteLabels notes; 202 | 203 | private: 204 | double m_notesLength; 205 | }; 206 | -------------------------------------------------------------------------------- /src/song.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "notes.hh" 7 | 8 | #include 9 | #include 10 | 11 | /// parsing of songfile failed 12 | struct SongParserException: public std::runtime_error { 13 | /// constructor 14 | SongParserException(QString const& msg, unsigned int linenum, bool sil = false) 15 | : runtime_error(msg.toStdString()), m_linenum(linenum), m_silent(sil) {} 16 | unsigned int line() const { return m_linenum; } ///< line in which the error occured 17 | bool silent() const { return m_silent; } ///< if the error should not be printed to user (file skipped) 18 | private: 19 | unsigned int m_linenum; 20 | bool m_silent; 21 | }; 22 | 23 | class SongParser; 24 | 25 | namespace TrackName { 26 | const QString GUITAR = "Guitar"; 27 | const QString GUITAR_COOP = "Coop guitar"; 28 | const QString GUITAR_RHYTHM = "Rhythm guitar"; 29 | const QString BASS = "Bass"; 30 | const QString DRUMS = "Drums"; 31 | const QString LEAD_VOCAL = "Vocals"; 32 | const QString HARMONIC_1 = "Harmonic 1"; 33 | const QString HARMONIC_2 = "Harmonic 2"; 34 | const QString HARMONIC_3 = "Harmonic 3"; 35 | } 36 | 37 | /// class to load and parse songfiles 38 | class Song { 39 | Q_DISABLE_COPY(Song); 40 | friend class SongParser; 41 | VocalTracks vocalTracks; ///< notes for the sing part 42 | VocalTrack dummyVocal; ///< notes for the sing part 43 | public: 44 | /// constructors 45 | Song(): dummyVocal(TrackName::LEAD_VOCAL) { reload(true); } 46 | Song(QString const& path_, QString const& filename_): dummyVocal(TrackName::LEAD_VOCAL), path(path_), filename(filename_) { reload(false); } 47 | /// reload song 48 | void reload(bool errorIgnore = true); 49 | /// parse field 50 | bool parseField(QString const& line); 51 | /// drop notes (to conserve memory), but keep info about available tracks 52 | void dropNotes(); 53 | /** Get formatted song label. **/ 54 | QString str() const { return title + " by " + artist; } 55 | /** Get full song information (used by the search function). **/ 56 | QString strFull() const { return title + "\n" + artist + "\n" + genre + "\n" + edition + "\n" + path; } 57 | /// Is the song parsed from the file yet? 58 | enum LoadStatus { NONE, HEADER, FULL } loadStatus; 59 | /// status of song 60 | enum Status { NORMAL, INSTRUMENTAL_BREAK, FINISHED }; 61 | /** Get the song status at a given timestamp **/ 62 | Status status(double time); 63 | int randomIdx; ///< sorting index used for random order 64 | void insertVocalTrack(QString vocalTrack, VocalTrack track) { 65 | vocalTracks.erase(vocalTrack); 66 | vocalTracks.insert(std::make_pair(vocalTrack, track)); //remove type specification to remove compile error 67 | } 68 | // Get a selected track, or LEAD_VOCAL if not found or the first one if not found 69 | VocalTrack& getVocalTrack(QString vocalTrack = TrackName::LEAD_VOCAL) { 70 | if(vocalTracks.find(vocalTrack) != vocalTracks.end()) { 71 | return vocalTracks.find(vocalTrack)->second; 72 | } else { 73 | if(vocalTracks.find(TrackName::LEAD_VOCAL) != vocalTracks.end()) { 74 | return vocalTracks.find(TrackName::LEAD_VOCAL)->second; 75 | } else { 76 | if(!vocalTracks.empty()) { 77 | return vocalTracks.begin()->second; 78 | } else { 79 | return dummyVocal; 80 | } 81 | } 82 | } 83 | } 84 | VocalTrack getVocalTrack(QString vocalTrack = TrackName::LEAD_VOCAL) const { 85 | if(vocalTracks.find(vocalTrack) != vocalTracks.end()) { 86 | return vocalTracks.find(vocalTrack)->second; 87 | } else { 88 | if(vocalTracks.find(TrackName::LEAD_VOCAL) != vocalTracks.end()) { 89 | return vocalTracks.find(TrackName::LEAD_VOCAL)->second; 90 | } else { 91 | if(!vocalTracks.empty()) { 92 | return vocalTracks.begin()->second; 93 | } else { 94 | return dummyVocal; 95 | } 96 | } 97 | } 98 | } 99 | 100 | std::vector getVocalTrackNames() { 101 | std::vector result; 102 | for (VocalTracks::const_iterator it = vocalTracks.begin(); it != vocalTracks.end(); ++it) { 103 | result.push_back(it->first); 104 | } 105 | return result; 106 | } 107 | //InstrumentTracks instrumentTracks; ///< guitar etc. notes for this song 108 | //DanceTracks danceTracks; ///< dance tracks 109 | //bool hasDance() const { return !danceTracks.empty(); } 110 | //bool hasDrums() const { return instrumentTracks.find(TrackName::DRUMS) != instrumentTracks.end(); } 111 | //bool hasGuitars() const { return instrumentTracks.size() - hasDrums(); } 112 | bool hasVocals() const { return !vocalTracks.empty(); } 113 | QString path; ///< path of songfile 114 | QString filename; ///< name of songfile 115 | QString midifilename; ///< name of midi file in FoF format 116 | std::vector category; ///< category of song 117 | QString genre; ///< genre 118 | QString edition; ///< license 119 | QString title; ///< songtitle 120 | QString artist; ///< artist 121 | QString text; ///< songtext 122 | QString creator; ///< creator 123 | QString language; ///< language 124 | QString year; ///< year 125 | QMap music; ///< music files (background, guitar, rhythm/bass, drums, vocals) 126 | QString cover; ///< cd cover 127 | QString background; ///< background image 128 | QString video; ///< video 129 | double bpm; ///< used for more accurate import --> export cycle 130 | /// Variables used for comparisons (sorting) 131 | QString collateByTitle; 132 | QString collateByTitleOnly; 133 | /// Variables used for comparisons (sorting) 134 | QString collateByArtist; 135 | QString collateByArtistOnly; 136 | /** Rebuild collate variables from other strings **/ 137 | void collateUpdate(); 138 | /** Convert a string to its collate form **/ 139 | static QString collate(QString const& str); 140 | double videoGap; ///< gap with video 141 | double start; ///< start of song 142 | double preview_start; ///< starting time for the preview 143 | 144 | typedef std::vector > Stops; 145 | Stops stops; ///< related to dance 146 | typedef std::vector Beats; 147 | Beats beats; ///< related to instrument and dance 148 | bool hasBRE; ///< is there a Big Rock Ending? (used for drums only) 149 | bool b0rkedTracks; ///< are some tracks broken? (so that user can be notified) 150 | struct SongSection { 151 | QString name; 152 | double begin; 153 | SongSection(QString const& name, const double begin): name(name), begin(begin) {} 154 | }; 155 | typedef std::vector SongSections; 156 | SongSections songsections; ///< vector of song sections 157 | bool getNextSection(double pos, SongSection §ion); 158 | bool getPrevSection(double pos, SongSection §ion); 159 | }; 160 | 161 | static inline bool operator<(Song const& l, Song const& r) { return l.collateByArtist < r.collateByArtist; } 162 | 163 | --------------------------------------------------------------------------------