diff --git a/src/app/qml/ExerciseView.qml b/src/app/qml/ExerciseView.qml index dc3237a..4305d97 100644 --- a/src/app/qml/ExerciseView.qml +++ b/src/app/qml/ExerciseView.qml @@ -1,373 +1,383 @@ /**************************************************************************** ** ** 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 . ** ****************************************************************************/ import QtQuick 2.7 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.3 import QtQuick.Window 2.0 Item { id: exerciseView visible: currentExercise != undefined property var currentExercise QtObject { id: internal property int currentAnswer property var colors: ["#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd", "#ccebc5", "#ffed6f", "#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6", "#6a3d9a", "#ffff99", "#b15928"] property Item rightAnswerRectangle property variant userAnswers: [] property bool answersAreRight onCurrentAnswerChanged: { for (var i = 0; i < yourAnswersParent.children.length; ++i) yourAnswersParent.children[i].destroy() yourAnswersParent.children = "" - for (var i = 0; i < currentAnswer; ++i) { - var newObject = answerOption.createObject(yourAnswersParent, {"model": userAnswers[i].model, "index": userAnswers[i].index, "position": i, "color": userAnswers[i].color, "border.width": 2}) - } + for (var i = 0; i < currentAnswer; ++i) + answerOption.createObject(yourAnswersParent, {"model": userAnswers[i].model, "index": userAnswers[i].index, "position": i, "color": userAnswers[i].color, "border.width": 2}) } } onCurrentExerciseChanged: { clearUserAnswers() for (var i = 0; i < answerGrid.children.length; ++i) answerGrid.children[i].destroy() answerGrid.children = "" if (currentExercise != undefined) { var currentExerciseOptions = currentExercise["options"]; if (currentExerciseOptions != undefined) { var length = currentExerciseOptions.length for (var i = 0; i < length; ++i) answerOption.createObject(answerGrid, {"model": currentExerciseOptions[i], "index": i, "color": internal.colors[i%24]}) } messageText.text = i18n("Click 'new question' to start!") exerciseView.state = "waitingForNewQuestion" } } function clearUserAnswers() { pianoView.clearAllMarks() for (var i = 0; i < yourAnswersParent.children.length; ++i) yourAnswersParent.children[i].destroy() yourAnswersParent.children = "" internal.currentAnswer = 0 internal.userAnswers = [] } function checkAnswers() { var rightAnswers = core.exerciseController.selectedExerciseOptions internal.answersAreRight = true for (var i = 0; i < currentExercise.numberOfSelectedOptions; ++i) { if (internal.userAnswers[i].name != rightAnswers[i].name) { yourAnswersParent.children[i].border.color = "red" internal.answersAreRight = false } else { yourAnswersParent.children[i].border.color = "green" } } messageText.text = (internal.answersAreRight) ? i18n("Congratulations, you answered correctly!"):i18n("Oops, not this time! Try again!") if (currentExercise.numberOfSelectedOptions == 1) highlightRightAnswer() else exerciseView.state = "waitingForNewQuestion" } function highlightRightAnswer() { var chosenExercises = core.exerciseController.selectedExerciseOptions for (var i = 0; i < answerGrid.children.length; ++i) { if (answerGrid.children[i].model.name != chosenExercises[0].name) { answerGrid.children[i].opacity = 0.25 } else { internal.rightAnswerRectangle = answerGrid.children[i] answerGrid.children[i].opacity = 1 } } internal.rightAnswerRectangle.model.sequence.split(' ').forEach(function(note) { pianoView.noteMark(0, core.exerciseController.chosenRootNote() + parseInt(note), 0, internal.rightAnswerRectangle.color) }) animation.start() } ColumnLayout { anchors.fill: parent spacing: Screen.width >= 1024 ? 20:10 Text { id: userMessage Layout.preferredWidth: parent.width horizontalAlignment: Text.AlignHCenter font.pointSize: Screen.width >= 1024 ? 18:14 anchors.horizontalCenter: parent.horizontalCenter wrapMode: Text.WordWrap text: (currentExercise != undefined) ? i18nc("technical term, do you have a musician friend?", currentExercise["userMessage"]):"" } Text { id: messageText font.pointSize: Screen.width >= 1024 ? 18:14 Layout.preferredWidth: parent.width horizontalAlignment: Text.AlignHCenter anchors.horizontalCenter: parent.horizontalCenter } Row { anchors.horizontalCenter: parent.horizontalCenter spacing: 10 Button { id: newPlayQuestionButton width: 120; height: 40 text: (exerciseView.state == "waitingForNewQuestion") ? i18n("new question"):i18n("play question") enabled: !animation.running onClicked: { if (exerciseView.state == "waitingForNewQuestion") { clearUserAnswers() messageText.text = "" core.exerciseController.randomlySelectExerciseOptions() var chosenExercises = core.exerciseController.selectedExerciseOptions core.soundController.prepareFromExerciseOptions(chosenExercises) if (currentExercise["playMode"] != "rhythm") pianoView.noteMark(0, core.exerciseController.chosenRootNote(), 0, "white") exerciseView.state = "waitingForAnswer" } core.soundController.play() } } Button { id: giveUpButton width: 120; height: 40 text: i18n("give up") enabled: exerciseView.state == "waitingForAnswer" && !animation.running onClicked: { - exerciseView.state = "waitingForNewQuestion" + var rightAnswers = core.exerciseController.selectedExerciseOptions + internal.userAnswers = [] + for (var i = 0; i < currentExercise.numberOfSelectedOptions; ++i) { + for (var j = 0; j < answerGrid.children.length; ++j) { + if (answerGrid.children[j].model.name == rightAnswers[i].name) { + internal.userAnswers.push({"name": rightAnswers[i].name, "model": answerGrid.children[j].model, "index": j, "color": internal.colors[j]}) + break + } + } + } + internal.currentAnswer = currentExercise.numberOfSelectedOptions + checkAnswers() } } } GroupBox { id: availableAnswers title: i18n("Available Answers") anchors.horizontalCenter: parent.horizontalCenter Layout.preferredWidth: parent.width Layout.fillHeight: true Flickable { anchors.fill: parent contentHeight: answerGrid.height clip: true Grid { id: answerGrid anchors.centerIn: parent spacing: 10 columns: Math.max(1, parent.width / (((currentExercise != undefined && currentExercise["playMode"] != "rhythm") ? 120:119) + spacing)) Component { id: answerOption Rectangle { id: answerRectangle property var model property int index property int position width: (currentExercise != undefined && currentExercise["playMode"] != "rhythm") ? 120:119 height: (currentExercise != undefined && currentExercise["playMode"] != "rhythm") ? 40:59 Text { id: option property string originalText: model.name visible: currentExercise != undefined && currentExercise["playMode"] != "rhythm" text: i18nc("technical term, do you have a musician friend?", model.name) width: parent.width - 4 anchors.centerIn: parent horizontalAlignment: Qt.AlignHCenter color: "black" wrapMode: Text.Wrap } Image { id: rhythmImage anchors.centerIn: parent visible: currentExercise != undefined && currentExercise["playMode"] == "rhythm" source: (currentExercise != undefined && currentExercise["playMode"] == "rhythm") ? "exercise-images/" + model.name + ".png":"" fillMode: Image.Pad } MouseArea { anchors.fill: parent onClicked: { if (exerciseView.state == "waitingForAnswer" && !animation.running) { onExited() internal.userAnswers.push({"name": option.originalText, "model": answerRectangle.model, "index": answerRectangle.index, "color": answerRectangle.color}) internal.currentAnswer++ if (internal.currentAnswer == currentExercise.numberOfSelectedOptions) checkAnswers() } } hoverEnabled: Qt.platform.os != "android" && !animation.running onEntered: { answerRectangle.color = Qt.darker(answerRectangle.color, 1.1) if (currentExercise["playMode"] != "rhythm") { if (parent.parent == answerGrid) { model.sequence.split(' ').forEach(function(note) { pianoView.noteMark(0, core.exerciseController.chosenRootNote() + parseInt(note), 0, internal.colors[answerRectangle.index]) }) } } else { var rightAnswers = core.exerciseController.selectedExerciseOptions if (parent.parent == yourAnswersParent && internal.userAnswers[position].name != rightAnswers[position].name) { parent.border.color = "green" for (var i = 0; i < answerGrid.children.length; ++i) { if (answerGrid.children[i].model.name == rightAnswers[position].name) { parent.color = answerGrid.children[i].color break } } rhythmImage.source = "exercise-images/" + rightAnswers[position].name + ".png" } } } onExited: { answerRectangle.color = internal.colors[answerRectangle.index] if (currentExercise["playMode"] != "rhythm") { if (parent.parent == answerGrid) { if (!animation.running) model.sequence.split(' ').forEach(function(note) { pianoView.noteUnmark(0, core.exerciseController.chosenRootNote() + parseInt(note), 0) }) } } else { var rightAnswers = core.exerciseController.selectedExerciseOptions if (parent.parent == yourAnswersParent && internal.userAnswers[position].name != rightAnswers[position].name) { parent.border.color = "red" parent.color = internal.userAnswers[position].color rhythmImage.source = "exercise-images/" + internal.userAnswers[position].name + ".png" } } } } } } } ScrollIndicator.vertical: ScrollIndicator { active: true } } } GroupBox { id: yourAnswers title: i18n("Your Answer(s)") Layout.preferredWidth: parent.width anchors.horizontalCenter: parent.horizontalCenter contentHeight: ((currentExercise != undefined && currentExercise["playMode"] != "rhythm") ? 40:59) Flickable { width: (currentExercise != undefined) ? Math.min(parent.width, internal.currentAnswer*130):0; height: parent.height anchors.horizontalCenter: parent.horizontalCenter contentWidth: (currentExercise != undefined) ? internal.currentAnswer*130:0 boundsBehavior: Flickable.StopAtBounds clip: true Row { id: yourAnswersParent anchors.centerIn: parent spacing: Screen.width >= 1024 ? 10:5 } ScrollIndicator.horizontal: ScrollIndicator { active: true } } } Button { id: backspaceButton text: i18n("backspace") anchors.horizontalCenter: parent.horizontalCenter visible: currentExercise != undefined && currentExercise["playMode"] == "rhythm" enabled: internal.currentAnswer > 0 && internal.currentAnswer < currentExercise.numberOfSelectedOptions onClicked: { internal.userAnswers.pop() internal.currentAnswer-- } } PianoView { id: pianoView visible: currentExercise != undefined && currentExercise["playMode"] != "rhythm" anchors.horizontalCenter: parent.horizontalCenter } } states: [ State { name: "waitingForNewQuestion" }, State { name: "waitingForAnswer" StateChangeScript { script: { for (var i = 0; i < answerGrid.children.length; ++i) { answerGrid.children[i].opacity = 1 } } } } ] ParallelAnimation { id: animation loops: 2 SequentialAnimation { PropertyAnimation { target: internal.rightAnswerRectangle; property: "rotation"; to: -45; duration: 200 } PropertyAnimation { target: internal.rightAnswerRectangle; property: "rotation"; to: 45; duration: 200 } PropertyAnimation { target: internal.rightAnswerRectangle; property: "rotation"; to: 0; duration: 200 } } SequentialAnimation { PropertyAnimation { target: internal.rightAnswerRectangle; property: "scale"; to: 1.2; duration: 300 } PropertyAnimation { target: internal.rightAnswerRectangle; property: "scale"; to: 1.0; duration: 300 } } onStopped: { exerciseView.state = "waitingForNewQuestion" } } Connections { target: core.exerciseController onSelectedExerciseOptionsChanged: pianoView.clearAllMarks() } } diff --git a/src/plugins/fluidsynthsoundcontroller/fluidsynthsoundcontroller.cpp b/src/plugins/fluidsynthsoundcontroller/fluidsynthsoundcontroller.cpp index 2b580a3..8be38b8 100644 --- a/src/plugins/fluidsynthsoundcontroller/fluidsynthsoundcontroller.cpp +++ b/src/plugins/fluidsynthsoundcontroller/fluidsynthsoundcontroller.cpp @@ -1,246 +1,246 @@ /**************************************************************************** ** ** 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 unsigned int FluidSynthSoundController::m_initialTime = 0; FluidSynthSoundController::FluidSynthSoundController(QObject *parent) : Minuet::ISoundController(parent), m_audioDriver(0), m_sequencer(0), m_song(0) { - m_tempo = 120; + m_tempo = 60; m_settings = new_fluid_settings(); fluid_settings_setstr(m_settings, "synth.reverb.active", "no"); fluid_settings_setstr(m_settings, "synth.chorus.active", "no"); m_synth = new_fluid_synth(m_settings); fluid_synth_cc(m_synth, 1, 100, 0); #ifdef Q_OS_LINUX int fluid_res = fluid_synth_sfload(m_synth, QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("soundfonts/GeneralUser-v1.47.sf2")).toLatin1(), 1); #endif #ifdef Q_OS_WIN int fluid_res = fluid_synth_sfload(m_synth, QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("minuet/soundfonts/GeneralUser-v1.47.sf2")).toLatin1(), 1); #endif if (fluid_res == FLUID_FAILED) qDebug() << "Error when loading soundfont!"; resetEngine(); } FluidSynthSoundController::~FluidSynthSoundController() { deleteEngine(); if (m_synth) delete_fluid_synth(m_synth); if (m_settings) delete_fluid_settings(m_settings); } 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) { qDebug() << "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) delete_fluid_sequencer(m_sequencer); if (m_audioDriver) delete_fluid_audio_driver(m_audioDriver); }