diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 17856ce..80f3cd8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,3 +1,4 @@ +add_subdirectory(utils) add_subdirectory(interfaces) add_subdirectory(plugins) add_subdirectory(app) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 20ed587..15d813c 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,84 +1,85 @@ set(minuet_SRCS main.cpp core.cpp uicontroller.cpp plugincontroller.cpp exercisecontroller.cpp ) qt5_add_resources(minuet_SRCS qml.qrc) IF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android") set(minuet_ICONS_PNG ${CMAKE_CURRENT_SOURCE_DIR}/icons/16-apps-minuet.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/22-apps-minuet.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/32-apps-minuet.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/48-apps-minuet.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/64-apps-minuet.png ${CMAKE_CURRENT_SOURCE_DIR}/icons/128-apps-minuet.png ) set(minuet_ICONS_SVG ${CMAKE_CURRENT_SOURCE_DIR}/icons/sc-apps-minuet.svgz ${CMAKE_CURRENT_SOURCE_DIR}/icons/16-apps-minuet.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/22-apps-minuet.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/32-apps-minuet.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/48-apps-minuet.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/64-apps-minuet.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/128-apps-minuet.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/16-actions-minuet-scales.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/16-actions-minuet-intervals.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/16-actions-minuet-chords.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/16-actions-minuet-rhythms.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/22-actions-minuet-scales.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/22-actions-minuet-intervals.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/22-actions-minuet-chords.svg ${CMAKE_CURRENT_SOURCE_DIR}/icons/22-actions-minuet-rhythms.svg ) ecm_add_app_icon(minuet_SRCS ICONS ${minuet_ICONS_PNG}) ecm_install_icons(ICONS ${minuet_ICONS_PNG} ${minuet_ICONS_SVG} DESTINATION ${ICON_INSTALL_DIR} THEME hicolor) ENDIF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android") if(ANDROID) include(../../cmake/qt-android-mk-apk.cmake) add_library(minuet SHARED ${minuet_SRCS}) qt_android_build_apk( TARGET ${PROJECT_NAME} PACKAGE_NAME org.kde.minuet QML_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR} ANDROID_EXTRA_FILES ${CMAKE_CURRENT_SOURCE_DIR}/../../android EXTRA_LIBS ../plugins/csoundsoundcontroller/libminuetcsoundsoundcontroller.so,../interfaces/libminuetinterfaces.so,../../src/plugins/csoundsoundcontroller/Csound-prefix/src/Csound/Android/CsoundAndroid/libs/arm64-v8a/libcsoundandroid.so,../../src/plugins/csoundsoundcontroller/Csound-prefix/src/Csound/Android/CsoundAndroid/libs/arm64-v8a/libsndfile.so,../../src/plugins/csoundsoundcontroller/Csound-prefix/src/Csound/Android/pluginlibs/libfluidsynth/libs/arm64-v8a/libfluidOpcodes.so,../../src/plugins/csoundsoundcontroller/Csound-prefix/src/Csound/Android/CsoundAndroid/libs/arm64-v8a/libc++_shared.so ) else() add_executable(minuet ${minuet_SRCS}) endif() target_link_libraries(minuet Qt5::Core Qt5::Gui Qt5::Qml Qt5::Quick Qt5::QuickControls2 Qt5::Svg Minuet::Interfaces + Minuet::Utils ) if(ANDROID) target_link_libraries(minuet Minuet::CsoundSoundController ) endif() IF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android") target_link_libraries(minuet KF5::CoreAddons KF5::I18n KF5::Crash ) ENDIF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android") IF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android") install(TARGETS minuet ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) install(PROGRAMS org.kde.minuet.desktop DESTINATION ${XDG_APPS_INSTALL_DIR}) ENDIF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android") diff --git a/src/app/exercisecontroller.cpp b/src/app/exercisecontroller.cpp index 1203802..6d03909 100644 --- a/src/app/exercisecontroller.cpp +++ b/src/app/exercisecontroller.cpp @@ -1,253 +1,266 @@ /**************************************************************************** ** ** Copyright (C) 2016 by Sandro S. Andrade ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the GNU General Public License as ** published by the Free Software Foundation; either version 2 of ** the License or (at your option) version 3 or any later version ** accepted by the membership of KDE e.V. (or its successor approved ** by the membership of KDE e.V.), which shall act as a proxy ** defined in Section 14 of version 3 of the license. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ** ****************************************************************************/ #include "exercisecontroller.h" #if !defined(Q_OS_ANDROID) #include #endif #include #include #include #include #include #include +#include + namespace Minuet { ExerciseController::ExerciseController(QObject *parent) : IExerciseController(parent), m_chosenRootNote(0) { m_exercises["exercises"] = QJsonArray(); m_definitions["definitions"] = QJsonArray(); } ExerciseController::~ExerciseController() { } bool ExerciseController::initialize(Core *core) { Q_UNUSED(core) m_errorString.clear(); bool definitionsMerge = mergeJsonFiles("definitions", m_definitions); bool exercisesMerge = mergeJsonFiles("exercises", m_exercises, true, "name", "children"); // QFile file("merged-exercises.json"); // file.open(QIODevice::WriteOnly); // file.write(QJsonDocument(m_exercises).toJson()); // file.close(); return definitionsMerge & exercisesMerge; } QString ExerciseController::errorString() const { return m_errorString; } void ExerciseController::randomlySelectExerciseOptions() { while (!m_selectedExerciseOptions.isEmpty()) m_selectedExerciseOptions.removeFirst(); int minNote = INT_MAX; int maxNote = INT_MIN; auto *generator = QRandomGenerator::global(); quint8 numberOfSelectedOptions = m_currentExercise[QStringLiteral("numberOfSelectedOptions")].toInt(); for (quint8 i = 0; i < numberOfSelectedOptions; ++i) { QJsonArray exerciseOptions = QJsonObject::fromVariantMap(m_currentExercise)[QStringLiteral("options")].toArray(); quint8 chosenExerciseOption = generator->bounded(exerciseOptions.size()); QString sequence = exerciseOptions[chosenExerciseOption].toObject()[QStringLiteral("sequence")].toString(); foreach(const QString &additionalNote, sequence.split(' ')) { int note = additionalNote.toInt(); if (note > maxNote) maxNote = note; if (note < minNote) minNote = note; } if (m_currentExercise["playMode"].toString() != "rhythm") { QStringList exerciseRoots = m_currentExercise["root"].toString().split('.'); quint8 exerciseMinRoot = exerciseRoots.first().toInt(); quint8 exerciseMaxRoot = exerciseRoots.last().toInt(); do m_chosenRootNote = exerciseMinRoot + generator->bounded(exerciseMaxRoot - exerciseMinRoot); while (m_chosenRootNote + maxNote > 108 || m_chosenRootNote + minNote < 21); } QJsonObject jsonObject = exerciseOptions[chosenExerciseOption].toObject(); jsonObject["rootNote"] = QString::number(m_chosenRootNote); exerciseOptions[chosenExerciseOption] = jsonObject; m_selectedExerciseOptions.append(exerciseOptions[chosenExerciseOption]); } emit selectedExerciseOptionsChanged(m_selectedExerciseOptions); } unsigned int ExerciseController::chosenRootNote() { return m_chosenRootNote; } QJsonArray ExerciseController::exercises() const { return m_exercises[QStringLiteral("exercises")].toArray(); } bool ExerciseController::mergeJsonFiles(const QString directoryName, QJsonObject &targetObject, bool applyDefinitionsFlag, QString commonKey, QString mergeKey) { QStringList jsonDirs; #if defined(Q_OS_ANDROID) jsonDirs << "assets:/data/" + directoryName; #elif defined(Q_OS_WIN) jsonDirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("minuet/") + directoryName, QStandardPaths::LocateDirectory); #else jsonDirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, directoryName, QStandardPaths::LocateDirectory); +#ifdef Q_OS_MACOS + if (jsonDirs.isEmpty()) { + const QStringList xdgDataDirs = Utils::getXdgDataDirs(); + for (const auto &dirPath : xdgDataDirs) { + const QDir testDir(QDir(dirPath).absoluteFilePath(QStringLiteral("minuet/") + directoryName)); + if (testDir.exists()) { + jsonDirs << testDir.absolutePath(); + } + } + } +#endif #endif foreach (const QString &jsonDirString, jsonDirs) { QDir jsonDir(jsonDirString); foreach (const QString &json, jsonDir.entryList(QDir::Files)) { if (!json.endsWith(QLatin1String(".json"))) break; QFile jsonFile(jsonDir.absoluteFilePath(json)); if (!jsonFile.open(QIODevice::ReadOnly)) { #if !defined(Q_OS_ANDROID) m_errorString = i18n("Could not open JSON file \"%1\".", jsonDir.absoluteFilePath(json)); #else m_errorString = QStringLiteral("Couldn't open json file \"%1\".").arg(jsonDir.absoluteFilePath(json)); #endif return false; } QJsonParseError error; QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonFile.readAll(), &error); if (error.error != QJsonParseError::NoError) { m_errorString += QStringLiteral("Error when parsing JSON file '%1'. ").arg(jsonDir.absoluteFilePath(json)); jsonFile.close(); return false; } else { QJsonObject jsonObject = jsonDocument.object(); if (applyDefinitionsFlag) jsonObject[directoryName] = applyDefinitions(jsonObject[directoryName].toArray(), m_definitions[QStringLiteral("definitions")].toArray()); targetObject[directoryName] = mergeJsonArrays(targetObject[directoryName].toArray(), jsonObject[directoryName].toArray(), commonKey, mergeKey); } jsonFile.close(); } } return true; } QJsonArray ExerciseController::applyDefinitions(QJsonArray exercises, QJsonArray definitions, QJsonObject collectedProperties) { QJsonArray::const_iterator exercisesBegin = exercises.constBegin(); QJsonArray::const_iterator exercisesEnd = exercises.constEnd(); for (QJsonArray::ConstIterator i1 = exercisesBegin; i1 < exercisesEnd; ++i1) { if (i1->isObject()) { QJsonObject exerciseObject = i1->toObject(); QJsonArray filteredDefinitions = definitions; QStringList exerciseObjectKeys = exerciseObject.keys(); if (exerciseObjectKeys.contains(QStringLiteral("and-tags")) && exerciseObject[QStringLiteral("and-tags")].isArray()) filterDefinitions(filteredDefinitions, exerciseObject, "and-tags", AndFiltering); if (exerciseObjectKeys.contains(QStringLiteral("or-tags")) && exerciseObject[QStringLiteral("or-tags")].isArray()) filterDefinitions(filteredDefinitions, exerciseObject, "or-tags", OrFiltering); if (exerciseObjectKeys.contains(QStringLiteral("children"))) { foreach(const QString &key, exerciseObjectKeys) if (key != "name" && key != "children" && key != "and-tags" && key != "or-tags" && !key.startsWith('_')) { collectedProperties.insert(key, exerciseObject[key]); exerciseObject.remove(key); } exerciseObject[QStringLiteral("children")] = applyDefinitions(exerciseObject[QStringLiteral("children")].toArray(), filteredDefinitions, collectedProperties); } else { foreach(const QString &key, collectedProperties.keys()) if (!exerciseObject.contains(key)) exerciseObject.insert(key, collectedProperties[key]); exerciseObject.insert("options", filteredDefinitions); } exercises[i1-exercisesBegin] = exerciseObject; } } return exercises; } void ExerciseController::filterDefinitions(QJsonArray &definitions, QJsonObject &exerciseObject, const QString &filterTagsKey, DefinitionFilteringMode definitionFilteringMode) { QJsonArray filterTags = exerciseObject[filterTagsKey].toArray(); exerciseObject.remove(filterTagsKey); for (QJsonArray::Iterator i2 = definitions.begin(); i2 < definitions.end(); ++i2) { bool remove = (definitionFilteringMode == AndFiltering) ? false:true; QJsonArray::const_iterator filterTagsEnd = filterTags.constEnd(); for (QJsonArray::ConstIterator i3 = filterTags.constBegin(); i3 < filterTagsEnd; ++i3) { QJsonArray tagArray = i2->toObject()["tags"].toArray(); if (definitionFilteringMode == AndFiltering && !tagArray.contains(*i3)) { remove = true; break; } if (definitionFilteringMode == OrFiltering && tagArray.contains(*i3)) remove = false; } if (remove) { i2 = definitions.erase(i2); i2--; } } } QJsonArray ExerciseController::mergeJsonArrays(QJsonArray oldFile, QJsonArray newFile, QString commonKey, QString mergeKey) { QJsonArray::const_iterator newFileEnd = newFile.constEnd();; for (QJsonArray::ConstIterator i1 = newFile.constBegin(); i1 < newFileEnd; ++i1) { if (i1->isObject()) { QJsonArray::ConstIterator i2; QJsonArray::const_iterator oldFileEnd = oldFile.constEnd(); for (i2 = oldFile.constBegin(); i2 < oldFileEnd; ++i2) { QJsonObject newFileObject = i1->toObject(); QJsonObject oldFileObject = i2->toObject(); if (i2->isObject() && i1->isObject() && !commonKey.isEmpty() && oldFileObject[commonKey] == newFileObject[commonKey]) { QJsonObject jsonObject = oldFile[i2-oldFile.constBegin()].toObject(); jsonObject[mergeKey] = mergeJsonArrays(oldFileObject[mergeKey].toArray(), newFileObject[mergeKey].toArray(), commonKey, mergeKey); oldFile[i2-oldFile.constBegin()] = jsonObject; break; } } if (i2 == oldFile.constEnd()) oldFile.append(*i1); } } return oldFile; } } diff --git a/src/plugins/fluidsynthsoundcontroller/CMakeLists.txt b/src/plugins/fluidsynthsoundcontroller/CMakeLists.txt index 2263789..2f3e43b 100644 --- a/src/plugins/fluidsynthsoundcontroller/CMakeLists.txt +++ b/src/plugins/fluidsynthsoundcontroller/CMakeLists.txt @@ -1,16 +1,17 @@ set(fluidsynthsoundcontroller_PLUGIN_SRCS fluidsynthsoundcontroller.cpp ) add_library(minuetfluidsynthsoundcontroller MODULE ${fluidsynthsoundcontroller_PLUGIN_SRCS}) target_link_libraries(minuetfluidsynthsoundcontroller Qt5::Core Minuet::Interfaces + Minuet::Utils FluidSynth::FluidSynth ) IF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android") install(TARGETS minuetfluidsynthsoundcontroller DESTINATION ${PLUGIN_INSTALL_DIR}/minuet/) install(FILES GeneralUser-v1.47.sf2 DESTINATION ${KDE_INSTALL_DATADIR}/minuet/soundfonts) ENDIF(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android") diff --git a/src/plugins/fluidsynthsoundcontroller/fluidsynthsoundcontroller.cpp b/src/plugins/fluidsynthsoundcontroller/fluidsynthsoundcontroller.cpp index 7252914..ef46bfe 100644 --- a/src/plugins/fluidsynthsoundcontroller/fluidsynthsoundcontroller.cpp +++ b/src/plugins/fluidsynthsoundcontroller/fluidsynthsoundcontroller.cpp @@ -1,260 +1,275 @@ /**************************************************************************** ** ** Copyright (C) 2016 by Sandro S. Andrade ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the GNU General Public License as ** published by the Free Software Foundation; either version 2 of ** the License or (at your option) version 3 or any later version ** accepted by the membership of KDE e.V. (or its successor approved ** by the membership of KDE e.V.), which shall act as a proxy ** defined in Section 14 of version 3 of the license. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ** ****************************************************************************/ #include "fluidsynthsoundcontroller.h" #include #include #include #include #include +#include + unsigned int FluidSynthSoundController::m_initialTime = 0; FluidSynthSoundController::FluidSynthSoundController(QObject *parent) : Minuet::ISoundController(parent), m_audioDriver(0), m_sequencer(0), m_song(0), m_unregisteringEvent(0) { m_tempo = 60; m_settings = new_fluid_settings(); fluid_settings_setint(m_settings, "synth.reverb.active", 0); fluid_settings_setint(m_settings, "synth.chorus.active", 0); m_synth = new_fluid_synth(m_settings); fluid_synth_cc(m_synth, 1, 100, 0); #ifdef Q_OS_WIN const QString sf_path = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("minuet/soundfonts/GeneralUser-v1.47.sf2")); #else - const QString sf_path = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("soundfonts/GeneralUser-v1.47.sf2")); + QString sf_path = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("soundfonts/GeneralUser-v1.47.sf2")); +#ifdef Q_OS_MACOS + if (sf_path.isEmpty()) { + const QStringList xdgDataDirs = Utils::getXdgDataDirs(); + for (const auto &dirPath : xdgDataDirs) { + const QFile testFile(QDir(dirPath).absoluteFilePath(QStringLiteral("minuet/soundfonts/GeneralUser-v1.47.sf2"))); + if (testFile.exists()) { + sf_path = testFile.fileName(); + break; + } + } + } +#endif #endif + int fluid_res = fluid_synth_sfload(m_synth, sf_path.toLatin1(), 1); if (fluid_res == FLUID_FAILED) - qCritical() << "Error when loading soundfont!"; + qCritical() << "Error when loading soundfont in:" << sf_path; m_unregisteringEvent = new_fluid_event(); fluid_event_set_source(m_unregisteringEvent, -1); resetEngine(); } FluidSynthSoundController::~FluidSynthSoundController() { deleteEngine(); if (m_synth) delete_fluid_synth(m_synth); if (m_settings) delete_fluid_settings(m_settings); if (m_unregisteringEvent) delete_fluid_event(m_unregisteringEvent); } void FluidSynthSoundController::setPitch(qint8 pitch) { m_pitch = pitch; fluid_synth_cc(m_synth, 1, 101, 0); fluid_synth_cc(m_synth, 1, 6, 12); float accurate_pitch = (m_pitch + 12) * (2.0 / 3) * 1024; fluid_synth_pitch_bend(m_synth, 1, qMin(qRound(accurate_pitch), 16 * 1024 - 1)); } void FluidSynthSoundController::setVolume(quint8 volume) { m_volume = volume; fluid_synth_cc(m_synth, 1, 7, m_volume * 127 / 200); } void FluidSynthSoundController::setTempo (quint8 tempo) { m_tempo = tempo; } void FluidSynthSoundController::prepareFromExerciseOptions(QJsonArray selectedExerciseOptions) { QList *song = new QList; m_song.reset(song); if (m_playMode == "rhythm") for (int i = 0; i < 4; ++i) appendEvent(9, 80, 127, 1000*(60.0/m_tempo)); for (int i = 0; i < selectedExerciseOptions.size(); ++i) { QString sequence = selectedExerciseOptions[i].toObject()[QStringLiteral("sequence")].toString(); unsigned int chosenRootNote = selectedExerciseOptions[i].toObject()[QStringLiteral("rootNote")].toString().toInt(); if (m_playMode != "rhythm") { appendEvent(1, chosenRootNote, 127, 1000*(60.0/m_tempo)); foreach(const QString &additionalNote, sequence.split(' ')) appendEvent(1, chosenRootNote + additionalNote.toInt(), 127, ((m_playMode == "scale") ? 1000:4000)*(60.0/m_tempo)); } else { //appendEvent(9, 80, 127, 1000*(60.0/m_tempo)); foreach(QString additionalNote, sequence.split(' ')) { // krazy:exclude=foreach float dotted = 1; if (additionalNote.endsWith('.')) { dotted = 1.5; additionalNote.chop(1); } unsigned int duration = dotted*1000*(60.0/m_tempo)*(4.0/additionalNote.toInt()); appendEvent(9, 37, 127, duration); } } } //if (m_playMode == "rhythm") // appendEvent(9, 80, 127, 1000*(60.0/m_tempo)); fluid_event_t *event = new_fluid_event(); fluid_event_set_source(event, -1); fluid_event_all_notes_off(event, 1); m_song->append(event); } void FluidSynthSoundController::prepareFromMidiFile(const QString &fileName) { Q_UNUSED(fileName) } void FluidSynthSoundController::play() { if (!m_song.data()) return; if (m_state != PlayingState) { unsigned int now = fluid_sequencer_get_tick(m_sequencer); foreach(fluid_event_t *event, *m_song.data()) { if (fluid_event_get_type(event) != FLUID_SEQ_ALLNOTESOFF || m_playMode != "chord") { fluid_event_set_dest(event, m_synthSeqID); fluid_sequencer_send_at(m_sequencer, event, now, 1); } fluid_event_set_dest(event, m_callbackSeqID); fluid_sequencer_send_at(m_sequencer, event, now, 1); now += (m_playMode == "rhythm") ? fluid_event_get_duration(event): (m_playMode == "scale") ? 1000*(60.0/m_tempo):0; } setState(PlayingState); } } void FluidSynthSoundController::pause() { } void FluidSynthSoundController::stop() { if (m_state != StoppedState) { fluid_event_t *event = new_fluid_event(); fluid_event_set_source(event, -1); fluid_event_all_notes_off(event, 1); fluid_event_set_dest(event, m_synthSeqID); fluid_sequencer_send_now(m_sequencer, event); resetEngine(); } } void FluidSynthSoundController::reset() { stop(); m_song.reset(0); } void FluidSynthSoundController::appendEvent(int channel, short key, short velocity, unsigned int duration) { fluid_event_t *event = new_fluid_event(); fluid_event_set_source(event, -1); fluid_event_note(event, channel, key, velocity, duration); m_song->append(event); } void FluidSynthSoundController::sequencerCallback(unsigned int time, fluid_event_t *event, fluid_sequencer_t *seq, void *data) { Q_UNUSED(seq); // This is safe! FluidSynthSoundController *soundController = reinterpret_cast(data); int eventType = fluid_event_get_type(event); switch (eventType) { case FLUID_SEQ_NOTE: { if (m_initialTime == 0) m_initialTime = time; double adjustedTime = (time - m_initialTime)/1000.0; int mins = adjustedTime / 60; int secs = ((int)adjustedTime) % 60; int cnts = 100*(adjustedTime-qFloor(adjustedTime)); static QChar fill('0'); soundController->setPlaybackLabel(QStringLiteral("%1:%2.%3").arg(mins, 2, 10, fill).arg(secs, 2, 10, fill).arg(cnts, 2, 10, fill)); break; } case FLUID_SEQ_ALLNOTESOFF: { m_initialTime = 0; soundController->setPlaybackLabel(QStringLiteral("00:00.00")); soundController->setState(StoppedState); break; } } } void FluidSynthSoundController::resetEngine() { deleteEngine(); #ifdef Q_OS_LINUX fluid_settings_setstr(m_settings, "audio.driver", "pulseaudio"); #endif #ifdef Q_OS_WIN fluid_settings_setstr(m_settings, "audio.driver", "dsound"); #endif m_audioDriver = new_fluid_audio_driver(m_settings, m_synth); if (!m_audioDriver) { fluid_settings_setstr(m_settings, "audio.driver", "alsa"); m_audioDriver = new_fluid_audio_driver(m_settings, m_synth); } if (!m_audioDriver) { qCritical() << "Couldn't start audio driver!"; } m_sequencer = new_fluid_sequencer2(0); m_synthSeqID = fluid_sequencer_register_fluidsynth(m_sequencer, m_synth); m_callbackSeqID = fluid_sequencer_register_client (m_sequencer, "Minuet Fluidsynth Sound Controller", &FluidSynthSoundController::sequencerCallback, this); m_initialTime = 0; setPlaybackLabel(QStringLiteral("00:00.00")); setState(StoppedState); } void FluidSynthSoundController::deleteEngine() { if (m_sequencer) { #if FLUIDSYNTH_VERSION_MAJOR >= 2 // explicit client unregistering required fluid_sequencer_unregister_client(m_sequencer, m_callbackSeqID); fluid_event_set_dest(m_unregisteringEvent, m_synthSeqID); fluid_event_unregistering(m_unregisteringEvent); fluid_sequencer_send_now(m_sequencer, m_unregisteringEvent); #endif delete_fluid_sequencer(m_sequencer); } if (m_audioDriver) delete_fluid_audio_driver(m_audioDriver); } diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt new file mode 100644 index 0000000..3671b8a --- /dev/null +++ b/src/utils/CMakeLists.txt @@ -0,0 +1,14 @@ +set(minuetutils_LIB_SRCS + xdgdatadirs.cpp +) + +add_library(minuetutils + STATIC + ${minuetutils_LIB_SRCS} +) +add_library(Minuet::Utils ALIAS minuetutils) + +target_link_libraries(minuetutils + PRIVATE + Qt5::Core +) diff --git a/src/utils/xdgdatadirs.cpp b/src/utils/xdgdatadirs.cpp new file mode 100644 index 0000000..d721bb1 --- /dev/null +++ b/src/utils/xdgdatadirs.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include "xdgdatadirs.h" + +using namespace Utils; + +QStringList Utils::getXdgDataDirs() { + const QString xdgDataDirsEnv = QFile::decodeName(qgetenv("XDG_DATA_DIRS")); + if (xdgDataDirsEnv.isEmpty()) { + return {}; + } + + QStringList results; + const auto paths = xdgDataDirsEnv.splitRef(QLatin1Char(':'), QString::SkipEmptyParts); + // Normalize paths, skip relative paths + for (const auto &path : paths) { + if (!QDir::isAbsolutePath(path.toString()) || !QDir(path.toString()).exists()) { + continue; + } + results.append(path.toString()); + } + + return results; +} diff --git a/src/utils/xdgdatadirs.h b/src/utils/xdgdatadirs.h new file mode 100644 index 0000000..b0be266 --- /dev/null +++ b/src/utils/xdgdatadirs.h @@ -0,0 +1,19 @@ +#ifndef MINUET_UTILS_XDGDATADIRS +#define MINUET_UTILS_XDGDATADIRS + +#include + +namespace Utils { + +/** + * @brief Get valid paths from XDG_DATA_DIRS environment variable + * Qt does not check XDG_DATA_DIRS for MACOS but KDE prefix.sh script sets it. + * If AppDataLocation fail, we should give a shot and check XDG env variable + * + * @return QStringList + */ +QStringList getXdgDataDirs(); + +} + +#endif