diff --git a/src/activities/note_names/ActivityInfo.qml b/src/activities/note_names/ActivityInfo.qml new file mode 100644 --- /dev/null +++ b/src/activities/note_names/ActivityInfo.qml @@ -0,0 +1,38 @@ +/* GCompris - ActivityInfo.qml + * + * Copyright (C) 2018 Aman Kumar Gupta + * + * Authors: + * Aman Kumar Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ +import GCompris 1.0 + +ActivityInfo { + name: "note_names/NoteNames.qml" + difficulty: 4 + icon: "note_names/note_names.svg" + author: "Aman Kumar Gupta <gupta2140@gmail.com>" + demo: true + title: qsTr("Name that Note") + description: qsTr("Learn the names of the notes, in bass and treble clef.") + //intro: "Identify the note and press the correct piano key" + goal: qsTr("To develop a good understanding of note position and naming convention. To prepare for the piano player and composition activity.") + prerequisite: qsTr("None") + manual: qsTr("Identify the notes correctly and score a 100% to complete a level.") + credit: "" + section: "discovery sound" + createdInVersion: 9500 +} diff --git a/src/activities/note_names/AdvancedTimer.qml b/src/activities/note_names/AdvancedTimer.qml new file mode 100644 --- /dev/null +++ b/src/activities/note_names/AdvancedTimer.qml @@ -0,0 +1,74 @@ +/* GCompris - AdvancedTimer.qml + * + * Copyright (C) 2018 Aman Kumar Gupta + * + * Authors: + * Aman Kumar Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ +import QtQuick 2.6 +import QtQuick.Controls 1.5 +import GCompris 1.0 +import "note_names.js" as Activity + +Timer { + id: timer + + property double startTime + property double pauseTime + property int timerNormalInterval: 2700 + property int remainingInterval + + interval: timerNormalInterval + + signal pause + signal resume + signal restart + + onPause: { + console.log(interval) + if(timer.running) { + pauseTime = new Date().getTime() + timer.stop() + } + } + + onResume: { + if(!timer.running) { + if(!triggeredOnStart) { + remainingInterval = Math.abs(timer.interval - Math.abs(pauseTime - startTime)) + timer.interval = remainingInterval + } + timer.start() + } + } + + onRestart: { + timer.stop() + timer.interval = 1 + timer.start() + } + + onTriggered:{ + if(interval != timerNormalInterval) { + interval = timerNormalInterval + } + } + + onRunningChanged: { + if(running) + startTime = new Date().getTime() + } +} diff --git a/src/activities/note_names/CMakeLists.txt b/src/activities/note_names/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/src/activities/note_names/CMakeLists.txt @@ -0,0 +1 @@ +GCOMPRIS_ADD_RCC(activities/note_names *.qml *.svg *.js resource/*) diff --git a/src/activities/note_names/NoteNames.qml b/src/activities/note_names/NoteNames.qml new file mode 100644 --- /dev/null +++ b/src/activities/note_names/NoteNames.qml @@ -0,0 +1,406 @@ +/* GCompris - NoteNames.qml + * + * Copyright (C) 2018 Aman Kumar Gupta + * + * Authors: + * Aman Kumar Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ +import QtQuick 2.6 +import QtQuick.Controls 1.5 +import GCompris 1.0 + +import "../../core" +import "../piano_composition" +import "note_names.js" as Activity + +ActivityBase { + id: activity + + onStart: focus = true + onStop: {} + + property bool horizontalLayout: width > height + + pageComponent: Rectangle { + id: background + anchors.fill: parent + color: "#ABCDEF" + + signal start + signal stop + + Component.onCompleted: { + activity.start.connect(start) + activity.stop.connect(stop) + } + + Keys.onPressed: { + if(!introMessage.visible && !iAmReady.visible && !messageBox.visible && multipleStaff.musicElementModel.count - 1) { + // If the key pressed matches the note, pass the correct answer as parameter, else pass a wrong answer. + if(event.key === Qt.Key_1) { + isCorrectKey('C') + } + else if(event.key === Qt.Key_2) { + isCorrectKey('D') + } + else if(event.key === Qt.Key_3) { + isCorrectKey('E') + } + else if(event.key === Qt.Key_4) { + isCorrectKey('F') + } + else if(event.key === Qt.Key_5) { + isCorrectKey('G') + } + else if(event.key === Qt.Key_6) { + isCorrectKey('A') + } + else if(event.key === Qt.Key_7) { + isCorrectKey('B') + } + else if(event.key === Qt.Key_Left && shiftKeyboardLeft.visible) { + doubleOctave.currentOctaveNb-- + } + else if(event.key === Qt.Key_Right && shiftKeyboardRight.visible) { + doubleOctave.currentOctaveNb++ + } + } + } + + function isCorrectKey(key) { + if(Activity.newNotesSequence[Activity.currentNoteIndex][0] === key) + Activity.correctAnswer() + else + items.displayNoteNameTimer.start() + } + + // Add here the QML items you need to access in javascript + QtObject { + id: items + property Item main: activity.main + property alias background: background + property GCSfx audioEffects: activity.audioEffects + property alias bar: bar + property alias multipleStaff: multipleStaff + property alias doubleOctave: doubleOctave + property alias bonus: bonus + property alias iAmReady: iAmReady + property alias messageBox: messageBox + property alias addNoteTimer: addNoteTimer + property alias dataset: dataset + property alias progressBar: progressBar + property alias introMessage: introMessage + property bool isTutorialMode: true + property alias displayNoteNameTimer: displayNoteNameTimer + } + + Loader { + id: dataset + asynchronous: false + source: "qrc:/gcompris/src/activities/note_names/resource/dataset_01.qml" + } + + onStart: { Activity.start(items) } + onStop: { Activity.stop() } + + property string clefType: "Treble" + + Timer { + id: displayNoteNameTimer + interval: 2000 + onRunningChanged: { + if(running) { + multipleStaff.pauseNoteAnimation() + addNoteTimer.pause() + messageBox.visible = true + } + else { + messageBox.visible = false + if(progressBar.percentage != 100 && Activity.newNotesSequence.length) { + Activity.wrongAnswer() + addNoteTimer.resume() + } + } + } + } + + Rectangle { + id: messageBox + width: label.width + 20 + height: label.height + 20 + border.width: 5 + border.color: "black" + anchors.centerIn: multipleStaff + radius: 10 + z: 11 + visible: false + onVisibleChanged: text = Activity.targetNotes[0] == undefined ? "" + : items.isTutorialMode ? qsTr("New note: %1").arg(Activity.targetNotes[0]) + : Activity.newNotesSequence[Activity.currentNoteIndex] + property string text + + GCText { + id: label + anchors.centerIn: parent + fontSize: mediumSize + text: parent.text + } + + MouseArea { + anchors.fill: parent + enabled: items.isTutorialMode + onClicked: { + items.multipleStaff.pauseNoteAnimation() + items.multipleStaff.musicElementModel.remove(1) + Activity.showTutorial() + } + } + } + + Rectangle { + id: colorLayer + anchors.fill: parent + color: "black" + opacity: 0.3 + visible: iAmReady.visible + z: 10 + MouseArea { + anchors.fill: parent + } + } + + ReadyButton { + id: iAmReady + focus: true + z: 10 + visible: !introMessage.visible + onVisibleChanged: { + messageBox.visible = false + } + onClicked: { + Activity.initLevel() + } + } + + IntroMessage { + id: introMessage + anchors { + top: parent.top + topMargin: 10 + right: parent.right + rightMargin: 5 + left: parent.left + leftMargin: 5 + } + z: 10 + } + + AdvancedTimer { + id: addNoteTimer + onTriggered: { + Activity.noteIndexToDisplay = (Activity.noteIndexToDisplay + 1) % Activity.newNotesSequence.length + Activity.displayNote(Activity.newNotesSequence[Activity.noteIndexToDisplay]) + } + } + + ProgressBar { + id: progressBar + height: 20 * ApplicationInfo.ratio + width: parent.width / 4 + + property int percentage: 0 + + value: percentage + maximumValue: 100 + visible: !items.isTutorialMode + anchors { + top: parent.top + topMargin: 10 + right: parent.right + rightMargin: 10 + } + + GCText { + anchors.centerIn: parent + fontSize: mediumSize + font.bold: true + color: "black" + //: %1 is replaced by a number (parent.value) and is followed by a % symbol. + text: qsTr("%1%").arg(parent.value) + z: 2 + } + } + + MultipleStaff { + id: multipleStaff + width: horizontalLayout ? parent.width * 0.5 : parent.width * 0.8 + height: horizontalLayout ? parent.height * 0.9 : parent.height * 0.7 + nbStaves: 1 + clef: clefType + notesColor: "red" + isFlickable: false + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: progressBar.height + 20 + flickableTopMargin: multipleStaff.height / 14 + distanceBetweenStaff / 2.7 + noteAnimationEnabled: true + onNoteAnimationFinished: { + if(!items.isTutorialMode) + displayNoteNameTimer.start() + } + } + + // We present a pair of two joint piano keyboard octaves. + Item { + id: doubleOctave + width: horizontalLayout ? parent.width * 0.8 : parent.width * 0.72 + height: horizontalLayout ? parent.height * 0.22 : 2 * parent.height * 0.18 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: bar.top + anchors.bottomMargin: 30 + + readonly property int nbJointKeyboards: 2 + readonly property int maxNbOctaves: (background.clefType === "Bass") ? 2 : 3 + property int currentOctaveNb: 0 + property var coloredKeyLabels: [] + + Repeater { + id: octaveRepeater + anchors.fill: parent + model: doubleOctave.nbJointKeyboards + PianoOctaveKeyboard { + id: pianoKeyboard + width: horizontalLayout ? octaveRepeater.width / 2 : octaveRepeater.width + height: horizontalLayout ? octaveRepeater.height : octaveRepeater.height / 2 + blackLabelsVisible: false + blackKeysEnabled: blackLabelsVisible + whiteKeysEnabled: !messageBox.visible && multipleStaff.musicElementModel.count > 1 + onNoteClicked: Activity.checkAnswer(note) + currentOctaveNb: doubleOctave.currentOctaveNb + anchors.top: (index === 1) ? octaveRepeater.top : undefined + anchors.topMargin: horizontalLayout ? 0 : -15 + anchors.bottom: (index === 0) ? octaveRepeater.bottom : undefined + anchors.right: (index === 1) ? octaveRepeater.right : undefined + coloredKeyLabels: doubleOctave.coloredKeyLabels + labelsColor: "red" + // The octaves sets corresponding to respective clef types are in pairs for the joint piano keyboards at a time when displaying. + whiteNotesBass: { + if(index === 0) { + return [ + whiteKeyNotes.slice(0, 4), // F1 to B1 + whiteKeyNotes.slice(11, 18) // C3 to B3 + ] + } + else { + return [ + whiteKeyNotes.slice(4, 11), // C2 to B2 + whiteKeyNotes.slice(18, 25) // C4 to B4 + ] + } + } + whiteNotesTreble: { + if(index === 0) { + return [ + whiteKeyNotes.slice(11, 18), // C3 to B3 + whiteKeyNotes.slice(18, 25), // C4 to B4 + whiteKeyNotes.slice(25, 32), // C5 to B5 + ] + } + else { + return [ + whiteKeyNotes.slice(18, 25), // C4 to B4 + whiteKeyNotes.slice(25, 32), // C5 to B5 + whiteKeyNotes.slice(32, 34), // C6 to D6 + ] + } + } + } + } + } + + Image { + id: shiftKeyboardLeft + source: "qrc:/gcompris/src/core/resource/bar_previous.svg" + sourceSize.width: horizontalLayout ? doubleOctave.width / 14 : doubleOctave.width / 7 + width: sourceSize.width + height: width + fillMode: Image.PreserveAspectFit + visible: (doubleOctave.currentOctaveNb > 0) && doubleOctave.visible + anchors { + verticalCenter: doubleOctave.verticalCenter + right: doubleOctave.left + } + MouseArea { + enabled: !messageBox.visible + anchors.fill: parent + onClicked: { + doubleOctave.currentOctaveNb-- + } + } + } + + Image { + id: shiftKeyboardRight + source: "qrc:/gcompris/src/core/resource/bar_next.svg" + sourceSize.width: horizontalLayout ? doubleOctave.width / 14 : doubleOctave.width / 7 + width: sourceSize.width + height: width + fillMode: Image.PreserveAspectFit + visible: (doubleOctave.currentOctaveNb < doubleOctave.maxNbOctaves - 1) && doubleOctave.visible + anchors { + verticalCenter: doubleOctave.verticalCenter + left: doubleOctave.right + } + MouseArea { + enabled: !messageBox.visible + anchors.fill: parent + onClicked: { + doubleOctave.currentOctaveNb++ + } + } + } + + OptionsRow { + id: optionsRow + visible: false + } + + DialogHelp { + id: dialogHelp + onClose: home() + } + + Bar { + id: bar + content: BarEnumContent { value: help | home | level | reload } + onHelpClicked: { + displayDialog(dialogHelp) + } + onPreviousLevelClicked: Activity.previousLevel() + onNextLevelClicked: Activity.nextLevel() + onHomeClicked: activity.home() + onReloadClicked: { + multipleStaff.eraseAllNotes() + iAmReady.visible = true + } + } + + Bonus { + id: bonus + Component.onCompleted: win.connect(Activity.nextLevel) + } + } +} diff --git a/src/activities/note_names/note_names.js b/src/activities/note_names/note_names.js new file mode 100644 --- /dev/null +++ b/src/activities/note_names/note_names.js @@ -0,0 +1,173 @@ +/* GCompris - note_names.js + * + * Copyright (C) 2018 Aman Kumar Gupta + * + * Authors: + * Aman Kumar Gupta + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ +.pragma library +.import QtQuick 2.6 as Quick +.import "qrc:/gcompris/src/core/core.js" as Core + +var currentLevel = 0 +var numberOfLevel +var dataset +var items +var levels +var targetNotes = [] +var newNotesSequence = [] +var currentNoteIndex +var noteIndexToDisplay +var percentageDecreaseValue = 4 +var percentageIncreaseValue = 2 + +function start(items_) { + items = items_ + currentLevel = 0 + dataset = items.dataset.item + levels = dataset.levels + numberOfLevel = levels.length + items.doubleOctave.coloredKeyLabels = dataset.referenceNotes[levels[0]["clef"]] + items.doubleOctave.currentOctaveNb = 1 + items.introMessage.intro = [dataset.objective] +} + +function stop() { + newNotesSequence = [] + items.multipleStaff.pauseNoteAnimation() + items.displayNoteNameTimer.stop() + items.addNoteTimer.stop() +} + +function initLevel() { + targetNotes = [] + newNotesSequence = [] + items.bar.level = currentLevel + 1 + items.background.clefType = levels[currentLevel]["clef"] + items.doubleOctave.coloredKeyLabels = dataset.referenceNotes[items.background.clefType] + items.doubleOctave.currentOctaveNb = 1 + items.multipleStaff.pauseNoteAnimation() + items.displayNoteNameTimer.stop() + items.addNoteTimer.stop() + items.multipleStaff.initClefs(items.background.clefType) + targetNotes = JSON.parse(JSON.stringify(levels[currentLevel]["sequence"])) + items.isTutorialMode = true + items.multipleStaff.coloredNotes = dataset.referenceNotes[items.background.clefType] + showTutorial() +} + +function showTutorial() { + items.messageBox.visible = false + if(targetNotes.length) { + displayNote(targetNotes[0]) + items.messageBox.visible = true + targetNotes.shift() + } + else { + items.isTutorialMode = false + startGame() + } +} + +// The principle is to fill half sequence (length 25) with the notes from previous levels and another half with current level's target notes and shuffle them. +function formNewNotesSequence() { + var halfSequenceLength = 25 + var fullSequenceLength = 50 + targetNotes = JSON.parse(JSON.stringify(levels[currentLevel]["sequence"])) + for(var i = 0; i < currentLevel && newNotesSequence.length < halfSequenceLength; i++) { + if(levels[currentLevel]["clef"] === levels[i]["clef"]) { + for(var j = 0; j < levels[i]["sequence"].length && newNotesSequence.length < halfSequenceLength; j++) + newNotesSequence.push(levels[i]["sequence"][j]) + } + } + + for(var i = 0; newNotesSequence.length && newNotesSequence.length < halfSequenceLength; i++) + newNotesSequence.push(newNotesSequence[i % newNotesSequence.length]) + + for(var i = 0; newNotesSequence.length < fullSequenceLength; i++) + newNotesSequence.push(targetNotes[i % targetNotes.length]) + + Core.shuffle(newNotesSequence) +} + +function startGame() { + currentNoteIndex = 0 + noteIndexToDisplay = 0 + items.progressBar.percentage = 0 + formNewNotesSequence() + displayNote(newNotesSequence[0]) +} + +function displayNote(currentNote) { + items.multipleStaff.addMusicElement("note", currentNote, "Quarter", false, false, items.background.clefType) + if(!items.isTutorialMode) { + items.addNoteTimer.interval = items.addNoteTimer.timerNormalInterval + items.addNoteTimer.start() + } +} + +function wrongAnswer() { + if(items.multipleStaff.musicElementRepeater.itemAt(1).x === (3 * items.multipleStaff.height / 25)) { + items.multipleStaff.musicElementModel.remove(1) + currentNoteIndex = (currentNoteIndex + 1) % newNotesSequence.length + } + + items.progressBar.percentage = Math.max(0, items.progressBar.percentage - percentageDecreaseValue) + items.multipleStaff.resumeNoteAnimation() + if(items.multipleStaff.musicElementModel.count <= 1) + items.addNoteTimer.restart() +} + +function correctAnswer() { + currentNoteIndex = (currentNoteIndex + 1) % newNotesSequence.length + items.multipleStaff.pauseNoteAnimation() + items.multipleStaff.musicElementModel.remove(1) + items.multipleStaff.resumeNoteAnimation() + items.progressBar.percentage += percentageIncreaseValue + if(items.progressBar.percentage === 100) { + items.multipleStaff.pauseNoteAnimation() + items.displayNoteNameTimer.stop() + items.addNoteTimer.stop() + items.bonus.good("flower") + } + else if(items.multipleStaff.musicElementModel.count <= 1) + items.addNoteTimer.restart() +} + +function checkAnswer(noteName) { + if(noteName === items.multipleStaff.musicElementModel.get(1).noteName_) + correctAnswer() + else + items.displayNoteNameTimer.start() +} + +function nextLevel() { + if(!items.iAmReady.visible) { + if(numberOfLevel <= ++ currentLevel) { + currentLevel = 0 + } + initLevel() + } +} + +function previousLevel() { + if(!items.iAmReady.visible) { + if(--currentLevel < 0) { + currentLevel = numberOfLevel - 1 + } + initLevel() + } +} diff --git a/src/activities/note_names/note_names.svg b/src/activities/note_names/note_names.svg new file mode 100644 --- /dev/null +++ b/src/activities/note_names/note_names.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/src/activities/note_names/resource/dataset_01.qml b/src/activities/note_names/resource/dataset_01.qml new file mode 100644 --- /dev/null +++ b/src/activities/note_names/resource/dataset_01.qml @@ -0,0 +1,105 @@ +/* GCompris - dataset_01.qml + * + * Copyright (C) 2018 Aman Kumar Gupta + * + * Authors: + * Beth Hadley (GTK+ version) + * Johnny Jazeix (Qt Quick port) + * Aman Kumar Gupta (Qt Quick port) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ +import QtQuick 2.1 + +QtObject { + property var levels: [ + { + "clef": "Treble", + "sequence": ["C4", "G4"] + }, + { + "clef": "Bass", + "sequence": ["C3", "F3"] + }, + { + "clef": "Treble", + "sequence": ["B3", "D4", "F4", "A4"] + }, + { + "clef": "Bass", + "sequence": ["B2", "D3","E3", "G3"] + }, + { + "clef": "Treble", + "sequence": ["C5", "G5"] + }, + { + "clef": "Bass", + "sequence": ["C2", "F2"] + }, + { + "clef": "Treble", + "sequence": ["B4", "D5", "F5", "A5"] + }, + { + "clef": "Bass", + "sequence": ["B1", "D2","E2", "G2"] + }, + { + "clef": "Treble", + "sequence": ["E4", "E5"] + }, + { + "clef": "Bass", + "sequence": ["B3", "D4"] + }, + { + "clef": "Treble", + "sequence": ["G3", "C6"] + }, + { + "clef": "Bass", + "sequence": ["A2", "A3", "C4", "E4"] + }, + { + "clef": "Treble", + "sequence": ["F3", "A3"] + }, + { + "clef": "Bass", + "sequence": ["G4", "A4"] + }, + { + "clef": "Treble", + "sequence": ["B5", "D5"] + }, + { + "clef": "Bass", + "sequence": ["B4"] + }, + { + "clef": "Treble", + "sequence": ["D3", "E3"] + }, + { + "clef": "Bass", + "sequence": ["F1", "G1", "A1"] + } + ] + property string objective: qsTr("You will learn notes from F1 to D6 in this lesson.
Note: Reference notes are red in color.") + property var referenceNotes: { + "Treble": ["C", "G"], + "Bass": ["F", "C"] + } +}