diff --git a/src/activities/note_names/AdvancedTimer.qml b/src/activities/note_names/AdvancedTimer.qml index 7a0879cd8..df0aaea16 100644 --- a/src/activities/note_names/AdvancedTimer.qml +++ b/src/activities/note_names/AdvancedTimer.qml @@ -1,73 +1,72 @@ /* 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 + interval: activity.timerNormalInterval signal pause signal resume signal restart onPause: { 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 + if(interval != activity.timerNormalInterval) { + interval = activity.timerNormalInterval } } onRunningChanged: { if(running) startTime = new Date().getTime() } } diff --git a/src/activities/note_names/NoteNames.qml b/src/activities/note_names/NoteNames.qml index 9926f4a90..3471ddb53 100644 --- a/src/activities/note_names/NoteNames.qml +++ b/src/activities/note_names/NoteNames.qml @@ -1,425 +1,497 @@ /* 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 + property int speedSetting: 5 + property int timerNormalInterval: (13500 / speedSetting) onStart: focus = true onStop: {} property bool horizontalLayout: width >= height pageComponent: Rectangle { id: background anchors.fill: parent color: "#ABCDEF" - signal start signal stop Component.onCompleted: { + dialogActivityConfig.getInitialConfiguration() activity.start.connect(start) activity.stop.connect(stop) } Keys.onPressed: { var keyNoteBindings = {} keyNoteBindings[Qt.Key_1] = 'C' keyNoteBindings[Qt.Key_2] = 'D' keyNoteBindings[Qt.Key_3] = 'E' keyNoteBindings[Qt.Key_4] = 'F' keyNoteBindings[Qt.Key_5] = 'G' keyNoteBindings[Qt.Key_6] = 'A' keyNoteBindings[Qt.Key_7] = 'B' if(!introMessage.visible && !iAmReady.visible && !messageBox.visible && multipleStaff.musicElementModel.count - 1) { if(keyNoteBindings[event.key]) { // If the key pressed matches the note, pass the correct answer as parameter. isCorrectKey(keyNoteBindings[event.key]) } 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) } + onStart: { Activity.start(items, activity.timerNormalInterval) } onStop: { Activity.stop() } property string clefType: "Treble" + DialogActivityConfig { + id: dialogActivityConfig + currentActivity: activity + content: Component { + Item { + property alias speedSlider: speedSlider + height: column.height + + Column { + id: column + spacing: 10 + width: parent.width + + Flow { + width: dialogActivityConfig.width + spacing: 5 + GCSlider { + id: speedSlider + width: 250 * ApplicationInfo.ratio + value: activity.speedSetting + maximumValue: 5 + minimumValue: 1 + scrollEnabled: false + } + GCText { + id: speedSliderText + text: qsTr("Speed") + fontSize: mediumSize + wrapMode: Text.WordWrap + } + } + } + } + } + + onStart: { + if(!introMessage.visible || !iAmReady.visible) { + multipleStaff.pauseNoteAnimation() + addNoteTimer.pause() + } + } + onClose: { + home(); + introMessage.visible = false; + iAmReady.visible = true; + } + onLoadData: { + if(dataToSave) { + if(dataToSave["speedSetting"]) { + activity.speedSetting = dataToSave["speedSetting"]; + } + } + } + onSaveData: { + var oldSpeed = activity.speedSetting + activity.speedSetting = dialogActivityConfig.configItem.speedSlider.value + if(oldSpeed != activity.speedSetting) { + dataToSave = {"speedSetting": activity.speedSetting}; + background.stop(); + introMessage.visible = false; + iAmReady.visible = true; + } + } + } + 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 function getTranslatedNoteName(noteName) { for(var i = 0; i < doubleOctave.keyNames.length; i++) { if(doubleOctave.keyNames[i][0] == noteName) return doubleOctave.keyNames[i][1] } return "" } onVisibleChanged: { if(Activity.targetNotes[0] === undefined) text = "" else if(items.isTutorialMode) text = qsTr("New note: %1").arg(getTranslatedNoteName(Activity.targetNotes[0])) else text = getTranslatedNoteName(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: 12 } 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" //: The following translation represents percentage. text: qsTr("%1%").arg(parent.value) z: 2 } } MultipleStaff { id: multipleStaff width: horizontalLayout ? parent.width * 0.5 : parent.width * 0.78 height: horizontalLayout ? parent.height * 0.9 : parent.height * 0.7 nbStaves: 1 clef: clefType notesColor: "red" softColorOpacity: 0 isFlickable: false anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: progressBar.height + 20 flickableTopMargin: multipleStaff.height / 14 + distanceBetweenStaff / 2.7 noteAnimationEnabled: true + noteAnimationDuration: items.isTutorialMode ? 9000 : 45000 / activity.speedSetting onNoteAnimationFinished: { if(!items.isTutorialMode) displayNoteNameTimer.start() } } // We present a pair of two joint piano keyboard octaves. Item { id: doubleOctave width: parent.width * 0.95 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: 3 property int currentOctaveNb: 0 property var coloredKeyLabels: [] property var keyNames: [] 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. whiteKeyNoteLabelsBass: { if(index === 0) { return [ whiteKeyNoteLabelsArray.slice(0, 4), // F1 to B1 whiteKeyNoteLabelsArray.slice(4, 11), // C2 to B2 whiteKeyNoteLabelsArray.slice(11, 18) // C3 to B3 ] } else { return [ whiteKeyNoteLabelsArray.slice(4, 11), // C2 to B2 whiteKeyNoteLabelsArray.slice(11, 18), // C3 to B3 whiteKeyNoteLabelsArray.slice(18, 25) // C4 to B4 ] } } whiteKeyNoteLabelsTreble: { if(index === 0) { return [ whiteKeyNoteLabelsArray.slice(11, 18), // C3 to B3 whiteKeyNoteLabelsArray.slice(18, 25), // C4 to B4 whiteKeyNoteLabelsArray.slice(25, 32) // C5 to B5 ] } else { return [ whiteKeyNoteLabelsArray.slice(18, 25), // C4 to B4 whiteKeyNoteLabelsArray.slice(25, 32), // C5 to B5 whiteKeyNoteLabelsArray.slice(32, 34) // C6 to D6 ] } } Component.onCompleted: doubleOctave.keyNames = whiteKeyNoteLabelsArray } } } Image { id: shiftKeyboardLeft source: "qrc:/gcompris/src/core/resource/bar_previous.svg" sourceSize.width: horizontalLayout ? doubleOctave.width / 13 : doubleOctave.width / 6 width: sourceSize.width height: width fillMode: Image.PreserveAspectFit visible: (doubleOctave.currentOctaveNb > 0) && doubleOctave.visible z: 11 anchors { bottom: doubleOctave.top left: doubleOctave.left leftMargin: -37 bottomMargin: horizontalLayout ? 10 : 25 } 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 / 13 : doubleOctave.width / 6 width: sourceSize.width height: width fillMode: Image.PreserveAspectFit visible: (doubleOctave.currentOctaveNb < doubleOctave.maxNbOctaves - 1) && doubleOctave.visible z: 11 anchors { bottom: doubleOctave.top right: doubleOctave.right rightMargin: -37 bottomMargin: horizontalLayout ? 10 : 25 } MouseArea { enabled: !messageBox.visible anchors.fill: parent onClicked: { doubleOctave.currentOctaveNb++ } } } OptionsRow { id: optionsRow iconsWidth: 0 visible: false } DialogHelp { id: dialogHelp onClose: home() } Bar { id: bar - content: BarEnumContent { value: help | home | level | reload } + content: BarEnumContent { value: (help | home | level | reload | config) } onHelpClicked: { displayDialog(dialogHelp) } onPreviousLevelClicked: Activity.previousLevel() onNextLevelClicked: Activity.nextLevel() onHomeClicked: activity.home() + onConfigClicked: { + dialogActivityConfig.active = true + displayDialog(dialogActivityConfig) + } onReloadClicked: { iAmReady.visible = true Activity.initLevel() } } 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 index 5846c4d26..8e94ce474 100644 --- a/src/activities/note_names/note_names.js +++ b/src/activities/note_names/note_names.js @@ -1,177 +1,179 @@ /* 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 +var timerNormalInterval -function start(items_) { +function start(items_, timerNormalInterval_) { items = items_ currentLevel = 0 + timerNormalInterval = timerNormalInterval_ 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] initLevel() } 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] if(items.background.clefType === "Treble") items.doubleOctave.currentOctaveNb = 1 else items.doubleOctave.currentOctaveNb = 2 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.progressBar.percentage = 0 items.multipleStaff.coloredNotes = dataset.referenceNotes[items.background.clefType] - if(!items.iAmReady.visible && ! items.introMessage.visible) + if(!items.iAmReady.visible && !items.introMessage.visible) showTutorial() } function showTutorial() { items.messageBox.visible = false if(targetNotes.length) { displayNote(targetNotes[0]) items.messageBox.visible = true targetNotes.shift() } - else { + else if (!items.iAmReady.visible) { 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) items.multipleStaff.playNoteAudio(currentNote, "Quarter", items.background.clefType, 500) if(!items.isTutorialMode) { - items.addNoteTimer.interval = items.addNoteTimer.timerNormalInterval + items.addNoteTimer.interval = timerNormalInterval items.addNoteTimer.start() } } function wrongAnswer() { if(items.multipleStaff.musicElementRepeater.itemAt(1).x <= items.multipleStaff.clefImageWidth) { 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(numberOfLevel <= ++ currentLevel) { currentLevel = 0 } initLevel() } function previousLevel() { if(--currentLevel < 0) { currentLevel = numberOfLevel - 1 } initLevel() } diff --git a/src/activities/piano_composition/MultipleStaff.qml b/src/activities/piano_composition/MultipleStaff.qml index 3f1aeb886..10f63ea29 100644 --- a/src/activities/piano_composition/MultipleStaff.qml +++ b/src/activities/piano_composition/MultipleStaff.qml @@ -1,605 +1,606 @@ /* GCompris - MultipleStaff.qml * * Copyright (C) 2016 Johnny Jazeix * 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.6 import GCompris 1.0 import "../../core" import "qrc:/gcompris/src/activities/piano_composition/NoteNotations.js" as NoteNotations Item { id: multipleStaff property int nbStaves property string clef property int distanceBetweenStaff: multipleStaff.height / 3.3 readonly property real clefImageWidth: 3 * height / 25 // Stores the note index which is selected. property int selectedIndex: -1 // The notes that are to be colored can be assigned to this variable in the activity property var coloredNotes: [] // When the notesColor is inbuilt, the default color mapping will be done, else assign any color externally in the activity. Example: Reference notes in note_names are red colored. property string notesColor: "inbuilt" property bool noteHoverEnabled: false // Stores if the notes are to be centered on the staff. Used in Play_piano and Play_rhythm. property bool centerNotesPosition: false property bool isPulseMarkerDisplayed: false property bool noteAnimationEnabled: false readonly property bool isMusicPlaying: musicTimer.running property alias flickableStaves: flickableStaves property alias musicElementModel: musicElementModel property alias musicElementRepeater: musicElementRepeater property double softColorOpacity : 0.8 property real flickableTopMargin: multipleStaff.height / 14 + distanceBetweenStaff / 3.5 readonly property real pulseMarkerX: pulseMarker.x readonly property bool isPulseMarkerRunning: pulseMarkerAnimation.running property bool isFlickable: true property bool enableNotesSound: true property int currentEnteringStaff: 0 property int bpmValue: 120 + property real noteAnimationDuration: 9000 // The position where the 1st note in the centered state is to be placed. property real firstCenteredNotePosition: multipleStaff.width / 3.3 property real spaceBetweenNotes: 0 /** * Emitted when a note is clicked. * * It is used for selecting note to play, erase and do other operations on it. */ signal noteClicked(int noteIndex) /** * Emitted when the animation of the note from the right of the staff to the left is finished. * * It's used in note_names activity. */ signal noteAnimationFinished /** * Emitted when the pulseMarker's animation is finished. */ signal pulseMarkerAnimationFinished /** * Used in play_rhythm activity. It tells the instants when pulseMarker reaches a note and the drum sound is to be played. */ signal playDrumSound ListModel { id: musicElementModel } Flickable { id: flickableStaves interactive: multipleStaff.isFlickable flickableDirection: Flickable.VerticalFlick contentWidth: staffColumn.width contentHeight: staffColumn.height + distanceBetweenStaff anchors.fill: parent clip: true Behavior on contentY { NumberAnimation { duration: 250 } } Column { id: staffColumn spacing: distanceBetweenStaff anchors.top: parent.top anchors.topMargin: flickableTopMargin Repeater { id: staves model: nbStaves Staff { id: staff height: multipleStaff.height / 5 width: multipleStaff.width - 5 lastPartition: index == (nbStaves - 1) } } } Repeater { id: musicElementRepeater model: musicElementModel MusicElement { id: musicElement noteName: noteName_ noteType: noteType_ highlightWhenPlayed: highlightWhenPlayed_ noteIsColored: multipleStaff.coloredNotes.indexOf(noteName[0]) != -1 soundPitch: soundPitch_ clefType: clefType_ elementType: elementType_ isDefaultClef: isDefaultClef_ property int staffNb: staffNb_ property alias noteAnimation: noteAnimation // The shift which the elements experience when a sharp/flat note is added before them. readonly property real sharpShiftDistance: blackType != "" ? width / 6 : 0 noteDetails: multipleStaff.getNoteDetails(noteName, noteType, clefType) MouseArea { id: noteMouseArea anchors.fill: parent hoverEnabled: true onClicked: multipleStaff.noteClicked(index) } function highlightNote() { highlightTimer.start() } readonly property real defaultXPosition: musicElementRepeater.itemAt(index - 1) ? (musicElementRepeater.itemAt(index - 1).width + musicElementRepeater.itemAt(index - 1).x) : 0 x: { if(multipleStaff.noteAnimationEnabled) return NaN // !musicElementRepeater.itemAt(index - 1) acts as a fallback condition when there is no previous element present. It happens when Qt clears the model internally. if(isDefaultClef || !musicElementRepeater.itemAt(index - 1)) return 0 else if(musicElementRepeater.itemAt(index - 1).elementType === "clef") { if(centerNotesPosition) return sharpShiftDistance + defaultXPosition + multipleStaff.firstCenteredNotePosition else return sharpShiftDistance + defaultXPosition + 10 } else return sharpShiftDistance + defaultXPosition + multipleStaff.spaceBetweenNotes } onYChanged: { if(noteAnimationEnabled && elementType === "note") noteAnimation.start() } y: { if(elementType === "clef") return flickableTopMargin + staves.itemAt(staffNb).y else if(noteDetails === undefined || staves.itemAt(staffNb) == undefined) return 0 var verticalDistanceBetweenLines = staves.itemAt(0).verticalDistanceBetweenLines var shift = -verticalDistanceBetweenLines / 2 var relativePosition = noteDetails.positionOnStaff var imageY = flickableTopMargin + staves.itemAt(staffNb).y + 2 * verticalDistanceBetweenLines if(rotation === 180) { return imageY - (4 - relativePosition) * verticalDistanceBetweenLines + shift } return imageY - (6 - relativePosition) * verticalDistanceBetweenLines + shift } NumberAnimation { id: noteAnimation target: musicElement properties: "x" - duration: 9000 + duration: noteAnimationDuration from: multipleStaff.width - 10 to: multipleStaff.clefImageWidth onStopped: { noteAnimationFinished() } } } } Image { id: secondStaffDefaultClef sourceSize.width: musicElementModel.count ? multipleStaff.clefImageWidth : 0 y: staves.count === 2 ? flickableTopMargin + staves.itemAt(1).y : 0 visible: (currentEnteringStaff === 0) && (nbStaves === 2) source: background.clefType ? "qrc:/gcompris/src/activities/piano_composition/resource/" + background.clefType.toLowerCase() + "Clef.svg" : "" } } Rectangle { id: pulseMarker width: activity.horizontalLayout ? 5 : 3 border.width: width / 2 height: staves.itemAt(0) == undefined ? 0 : 4 * staves.itemAt(0).verticalDistanceBetweenLines + width opacity: isPulseMarkerDisplayed && pulseMarkerAnimation.running color: "red" y: flickableTopMargin property real nextPosition: 0 NumberAnimation { id: pulseMarkerAnimation target: pulseMarker property: "x" to: pulseMarker.nextPosition onStarted: { if(pulseMarker.height == 0 && staves.count != 0) { pulseMarker.height = Qt.binding(function() {return 4 * staves.itemAt(0).verticalDistanceBetweenLines + pulseMarker.width;}) } } onStopped: { if(pulseMarker.x === multipleStaff.width) pulseMarkerAnimationFinished() else playDrumSound() } } } /** * Initializes the default clefs on the staves. * * @param clefType: The clef type to be initialized. */ function initClefs(clefType) { musicElementModel.clear() musicElementModel.append({ "elementType_": "clef", "clefType_": clefType, "staffNb_": 0, "isDefaultClef_": true, "noteName_": "", "noteType_": "", "soundPitch_": clefType, "highlightWhenPlayed_": false }) } /** * Pauses the sliding animation of the notes. */ function pauseNoteAnimation() { for(var i = 0; i < musicElementModel.count; i++) { if(musicElementRepeater.itemAt(i).noteAnimation.running) musicElementRepeater.itemAt(i).noteAnimation.pause() } } function resumeNoteAnimation() { for(var i = 0; i < musicElementModel.count; i++) { musicElementRepeater.itemAt(i).noteAnimation.resume() } } /** * Gets all the details of any note like note image, position on staff etc. from NoteNotations. */ function getNoteDetails(noteName, noteType, clefType) { var notesDetails = NoteNotations.get() var noteNotation if(noteType === "Rest") noteNotation = noteName + noteType else noteNotation = clefType + noteName for(var i = 0; i < notesDetails.length; i++) { if(noteNotation === notesDetails[i].noteName) { return notesDetails[i] } } } /** * Adds a note to the staff. */ function addMusicElement(elementType, noteName, noteType, highlightWhenPlayed, playAudio, clefType, soundPitch, isUnflicked) { if(soundPitch == undefined || soundPitch === "") soundPitch = clefType var isNextStaff = (selectedIndex == -1) && musicElementModel.count && ((staves.itemAt(0).width - musicElementRepeater.itemAt(musicElementModel.count - 1).x - musicElementRepeater.itemAt(musicElementModel.count - 1).width) < multipleStaff.clefImageWidth) // If the incoming element is a clef, make sure that there is enough required space to fit one more note too. Else it creates problem when the note is erased and the view is redrawn, else move on to the next staff. if(elementType === "clef" && musicElementModel.count && (selectedIndex == -1)) { if(staves.itemAt(0).width - musicElementRepeater.itemAt(musicElementModel.count - 1).x - musicElementRepeater.itemAt(musicElementModel.count - 1).width - 2 * Math.max(multipleStaff.clefImageWidth, musicElementRepeater.itemAt(0).noteImageWidth) < 0) isNextStaff = true } if(isNextStaff && !noteAnimationEnabled) { multipleStaff.currentEnteringStaff++ if(multipleStaff.currentEnteringStaff >= multipleStaff.nbStaves) multipleStaff.nbStaves++ // When a new staff is added, initialise it with a default clef. musicElementModel.append({"noteName_": "", "noteType_": "", "soundPitch_": soundPitch, "clefType_": clefType, "highlightWhenPlayed_": false, "staffNb_": multipleStaff.currentEnteringStaff, "isDefaultClef_": true, "elementType_": "clef"}) if(!isUnflicked) flickableStaves.flick(0, - nbStaves * multipleStaff.height) if(elementType === "clef") return 0 isNextStaff = false } if(selectedIndex === -1) { var isDefaultClef = false if(!musicElementModel.count) isDefaultClef = true musicElementModel.append({"noteName_": noteName, "noteType_": noteType, "soundPitch_": soundPitch, "clefType_": clefType, "highlightWhenPlayed_": highlightWhenPlayed, "staffNb_": multipleStaff.currentEnteringStaff, "isDefaultClef_": isDefaultClef, "elementType_": elementType}) } else { var tempModel = createNotesBackup() var insertingIndex = selectedIndex + 1 if(elementType === "clef") insertingIndex-- tempModel.splice(insertingIndex, 0, {"elementType_": elementType, "noteName_": noteName, "noteType_": noteType, "soundPitch_": soundPitch, "clefType_": clefType }) if(elementType === "clef") { for(var i = 0; i < musicElementModel.count && tempModel[i]["elementType_"] != "clef"; i++) tempModel[i]["soundPitch_"] = clefType } selectedIndex = -1 redraw(tempModel) } multipleStaff.selectedIndex = -1 background.clefType = musicElementModel.get(musicElementModel.count - 1).soundPitch_ if(playAudio) playNoteAudio(noteName, noteType, soundPitch, musicElementRepeater.itemAt(musicElementModel.count - 1).duration) } /** * Creates a backup of the musicElementModel before erasing it. * * This backup data is used to redraw the notes. */ function createNotesBackup() { var tempModel = [] for(var i = 0; i < musicElementModel.count; i++) tempModel.push(JSON.parse(JSON.stringify(musicElementModel.get(i)))) return tempModel } /** * Redraws all the notes on the staves. */ function redraw(notes) { musicElementModel.clear() currentEnteringStaff = 0 selectedIndex = -1 for(var i = 0; i < notes.length; i++) { var note = notes[i] // On load melody from file, the first "note" is the BPM value if(note.bpm) { bpmValue = note.bpm; } else { addMusicElement(note["elementType_"], note["noteName_"], note["noteType_"], false, false, note["clefType_"], note["soundPitch_"], true) } } // Remove the remaining unused staffs. if((multipleStaff.currentEnteringStaff + 1 < multipleStaff.nbStaves) && (multipleStaff.nbStaves > 2)) { nbStaves = multipleStaff.currentEnteringStaff + 1 flickableStaves.flick(0, - nbStaves * multipleStaff.height) } var lastMusicElement = musicElementModel.get(musicElementModel.count - 1) if(lastMusicElement.isDefaultClef_ && nbStaves > 2) { musicElementModel.remove(musicElementModel.count - 1) lastMusicElement = musicElementModel.get(musicElementModel.count - 1) } if(lastMusicElement.staffNb_ < nbStaves - 1 && nbStaves != 2) nbStaves = lastMusicElement.staffNb_ + 1 currentEnteringStaff = lastMusicElement.staffNb_ background.clefType = lastMusicElement.soundPitch_ } /** * Erases the selected note. * * @param noteIndex: index of the note to be erased */ function eraseNote(noteIndex) { musicElementModel.remove(noteIndex) selectedIndex = -1 var tempModel = createNotesBackup() redraw(tempModel) } /** * Erases all the notes. */ function eraseAllNotes() { musicElementModel.clear() selectedIndex = -1 multipleStaff.currentEnteringStaff = 0 initClefs(background.clefType) } readonly property var octave1MidiNumbersTable: {"C":24,"C#":25,"Db":25,"D":26,"D#":27,"Eb":27,"E":28,"F":29,"F#":30,"Gb":30,"G":31,"G#":32,"Ab":32,"A":33,"A#":34,"Bb":34,"B":35} /** * Plays audio for a note. * * @param noteName: name of the note to be played. * @param noteType: note type to be played. */ function playNoteAudio(noteName, noteType, soundPitch, duration) { if(noteName) { if(noteType != "Rest") { // We should find a corresponding b type enharmonic notation for # type note to play the audio. if(noteName[1] === "#") { var blackKeysFlat = piano.blackKeyFlatNoteLabelsArray var blackKeysSharp = piano.blackKeySharpNoteLabelsArray for(var i = 0; i < blackKeysSharp.length; i++) { if(blackKeysSharp[i][0] === noteName) { noteName = blackKeysFlat[i][0] break } } } var octaveNb = "" var noteCharName = "" if(noteName[1] == "#" || noteName[1] == "b") { noteCharName = noteName[0] + noteName[1] octaveNb = noteName[2] } else { noteCharName = noteName[0] octaveNb = noteName[1] } var noteMidiName = (octaveNb-1)*12 + octave1MidiNumbersTable[noteCharName]; GSynth.generate(noteMidiName, duration) } } } /** * Get all the notes from the musicElementModel and returns the melody. */ function getAllNotes() { var notes = createNotesBackup() return notes } /** * Loads melody from the provided data, to the staffs. * * @param data: melody to be loaded */ function loadFromData(data) { if(data != undefined) { var melody = data.split(" ") background.clefType = melody[0] eraseAllNotes() for(var i = 1 ; i < melody.length; ++i) { var noteLength = melody[i].length var noteName = melody[i][0] var noteType if(melody[i].substring(noteLength - 4, noteLength) === "Rest") { noteName = melody[i].substring(0, noteLength - 4) noteType = "Rest" } else if(melody[i][1] === "#" || melody[i][1] === "b") { noteType = melody[i].substring(3, melody[i].length) noteName += melody[i][1] + melody[i][2]; } else { noteType = melody[i].substring(2, melody[i].length) noteName += melody[i][1] } addMusicElement("note", noteName, noteType, false, false, melody[0]) } var tempModel = createNotesBackup() redraw(tempModel) } } /** * Used in the activity play_piano. * * Checks if the answered note is correct */ function indicateAnsweredNote(isCorrectAnswer, noteIndexAnswered) { musicElementRepeater.itemAt(noteIndexAnswered).noteAnswered = true musicElementRepeater.itemAt(noteIndexAnswered).isCorrectlyAnswered = isCorrectAnswer } /** * Used in the activity play_piano. * * Reverts the previous answer. */ function revertAnswer(noteIndexReverting) { musicElementRepeater.itemAt(noteIndexReverting).noteAnswered = false } function play() { musicTimer.currentNote = 0 selectedIndex = -1 musicTimer.interval = 1 if(isFlickable) flickableStaves.flick(0, nbStaves * multipleStaff.height) pulseMarkerAnimation.stop() if(musicElementModel.count > 1) pulseMarker.x = musicElementRepeater.itemAt(1).x + musicElementRepeater.itemAt(1).width / 2 else pulseMarker.x = 0 musicTimer.start() } /** * Stops the audios playing. */ function stopAudios() { musicElementModel.clear() musicTimer.stop() items.audioEffects.stop() } Timer { id: musicTimer property int currentNote: 0 onRunningChanged: { if(!running && musicElementModel.get(currentNote) !== undefined) { var currentElement = musicElementModel.get(currentNote) var currentType = currentElement.noteType_ var note = currentElement.noteName_ var soundPitch = currentElement.soundPitch_ var currentStaff = currentElement.staffNb_ background.clefType = currentElement.clefType_ if(currentElement.isDefaultClef_ && currentStaff > 1) { flickableStaves.contentY = staves.itemAt(currentStaff - 1).y } musicTimer.interval = musicElementRepeater.itemAt(currentNote).duration if(multipleStaff.enableNotesSound) playNoteAudio(note, currentType, soundPitch, musicTimer.interval) pulseMarkerAnimation.stop() pulseMarkerAnimation.duration = Math.max(1, musicTimer.interval) if(musicElementRepeater.itemAt(currentNote + 1) != undefined) pulseMarker.nextPosition = musicElementRepeater.itemAt(currentNote + 1).x + musicElementRepeater.itemAt(currentNote + 1).width / 2 else pulseMarker.nextPosition = multipleStaff.width pulseMarkerAnimation.start() if(!isPulseMarkerDisplayed) musicElementRepeater.itemAt(currentNote).highlightNote() currentNote++ musicTimer.start() } } } }