Warning, /education/gcompris/src/activities/piano_composition/MultipleStaff.qml is written in an unsupported language. File is not indexed.
0001 /* GCompris - MultipleStaff.qml 0002 * 0003 * SPDX-FileCopyrightText: 2016 Johnny Jazeix <jazeix@gmail.com> 0004 * SPDX-FileCopyrightText: 2018 Aman Kumar Gupta <gupta2140@gmail.com> 0005 * 0006 * Authors: 0007 * Beth Hadley <bethmhadley@gmail.com> (GTK+ version) 0008 * Johnny Jazeix <jazeix@gmail.com> (Qt Quick port) 0009 * Aman Kumar Gupta <gupta2140@gmail.com> (Qt Quick port) 0010 * 0011 * SPDX-License-Identifier: GPL-3.0-or-later 0012 */ 0013 import QtQuick 2.12 0014 import GCompris 1.0 0015 0016 import "../../core" 0017 import "qrc:/gcompris/src/activities/piano_composition/NoteNotations.js" as NoteNotations 0018 0019 Item { 0020 id: multipleStaff 0021 0022 property int nbStaves 0023 property string clef 0024 property int distanceBetweenStaff: multipleStaff.height / 3.3 0025 readonly property real clefImageWidth: 3 * height / 25 0026 0027 // Stores the note index which is selected. 0028 property int selectedIndex: -1 0029 0030 // The notes that are to be colored can be assigned to this variable in the activity 0031 property var coloredNotes: [] 0032 0033 // 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. 0034 property string notesColor: "inbuilt" 0035 property bool noteHoverEnabled: false 0036 0037 // Stores if the notes are to be centered on the staff. Used in Play_piano and Play_rhythm. 0038 property bool centerNotesPosition: false 0039 property bool isPulseMarkerDisplayed: false 0040 property bool noteAnimationEnabled: false 0041 readonly property bool isMusicPlaying: musicTimer.running 0042 0043 property alias flickableStaves: flickableStaves 0044 property alias musicElementModel: musicElementModel 0045 property alias musicElementRepeater: musicElementRepeater 0046 property double softColorOpacity : 0.8 0047 property real flickableTopMargin: multipleStaff.height / 14 + distanceBetweenStaff / 3.5 0048 readonly property real pulseMarkerX: pulseMarker.x 0049 readonly property bool isPulseMarkerRunning: pulseMarkerAnimation.running 0050 property bool isFlickable: true 0051 property bool enableNotesSound: true 0052 property int currentEnteringStaff: 0 0053 property int bpmValue: 120 0054 property real noteAnimationDuration: 9000 0055 0056 // The position where the 1st note in the centered state is to be placed. 0057 property real firstCenteredNotePosition: multipleStaff.width / 3.3 0058 property real spaceBetweenNotes: 0 0059 0060 /** 0061 * Emitted when a note is clicked. 0062 * 0063 * It is used for selecting note to play, erase and do other operations on it. 0064 */ 0065 signal noteClicked(int noteIndex) 0066 0067 /** 0068 * Emitted when the animation of the note from the right of the staff to the left is finished. 0069 * 0070 * It's used in note_names activity. 0071 */ 0072 signal noteAnimationFinished 0073 0074 /** 0075 * Emitted when the pulseMarker's animation is finished. 0076 */ 0077 signal pulseMarkerAnimationFinished 0078 0079 /** 0080 * Used in play_rhythm activity. It tells the instants when pulseMarker reaches a note and the drum sound is to be played. 0081 */ 0082 signal playDrumSound 0083 0084 ListModel { 0085 id: musicElementModel 0086 } 0087 0088 Flickable { 0089 id: flickableStaves 0090 interactive: multipleStaff.isFlickable 0091 flickableDirection: Flickable.VerticalFlick 0092 contentWidth: staffColumn.width 0093 contentHeight: staffColumn.height + distanceBetweenStaff 0094 anchors.fill: parent 0095 clip: true 0096 Behavior on contentY { 0097 NumberAnimation { duration: 250 } 0098 } 0099 0100 Column { 0101 id: staffColumn 0102 spacing: distanceBetweenStaff 0103 anchors.top: parent.top 0104 anchors.topMargin: flickableTopMargin 0105 Repeater { 0106 id: staves 0107 model: nbStaves 0108 Staff { 0109 id: staff 0110 height: multipleStaff.height / 5 0111 width: multipleStaff.width - 5 0112 lastPartition: index == (nbStaves - 1) 0113 } 0114 } 0115 } 0116 0117 Repeater { 0118 id: musicElementRepeater 0119 model: musicElementModel 0120 MusicElement { 0121 id: musicElement 0122 noteName: noteName_ 0123 noteType: noteType_ 0124 highlightWhenPlayed: highlightWhenPlayed_ 0125 noteIsColored: multipleStaff.coloredNotes.indexOf(noteName[0]) != -1 0126 soundPitch: soundPitch_ 0127 clefType: clefType_ 0128 elementType: elementType_ 0129 isDefaultClef: isDefaultClef_ 0130 0131 property int staffNb: staffNb_ 0132 property alias noteAnimation: noteAnimation 0133 // The shift which the elements experience when a sharp/flat note is added before them. 0134 readonly property real sharpShiftDistance: blackType != "" ? width / 6 : 0 0135 0136 noteDetails: multipleStaff.getNoteDetails(noteName, noteType, clefType) 0137 0138 MouseArea { 0139 id: noteMouseArea 0140 anchors.fill: parent 0141 hoverEnabled: true 0142 onClicked: multipleStaff.noteClicked(index) 0143 } 0144 0145 function highlightNote() { 0146 highlightTimer.start() 0147 } 0148 0149 readonly property real defaultXPosition: musicElementRepeater.itemAt(index - 1) ? (musicElementRepeater.itemAt(index - 1).width + musicElementRepeater.itemAt(index - 1).x) 0150 : 0 0151 0152 x: { 0153 if(multipleStaff.noteAnimationEnabled) 0154 return NaN 0155 // !musicElementRepeater.itemAt(index - 1) acts as a fallback condition when there is no previous element present. It happens when Qt clears the model internally. 0156 if(isDefaultClef || !musicElementRepeater.itemAt(index - 1)) 0157 return 0 0158 else if(musicElementRepeater.itemAt(index - 1).elementType === "clef") { 0159 if(centerNotesPosition) 0160 return sharpShiftDistance + defaultXPosition + multipleStaff.firstCenteredNotePosition 0161 else 0162 return sharpShiftDistance + defaultXPosition + 10 0163 } 0164 else 0165 return sharpShiftDistance + defaultXPosition + multipleStaff.spaceBetweenNotes 0166 } 0167 0168 onYChanged: { 0169 if(noteAnimationEnabled && elementType === "note") 0170 noteAnimation.start() 0171 } 0172 0173 y: { 0174 if(elementType === "clef") 0175 return flickableTopMargin + staves.itemAt(staffNb).y 0176 else if(noteDetails === undefined || staves.itemAt(staffNb) == undefined) 0177 return 0 0178 0179 var verticalDistanceBetweenLines = staves.itemAt(0).verticalDistanceBetweenLines 0180 var shift = -verticalDistanceBetweenLines / 2 0181 var relativePosition = noteDetails.positionOnStaff 0182 var imageY = flickableTopMargin + staves.itemAt(staffNb).y + 2 * verticalDistanceBetweenLines 0183 0184 if(rotation === 180) { 0185 return imageY - (4 - relativePosition) * verticalDistanceBetweenLines + shift 0186 } 0187 0188 return imageY - (6 - relativePosition) * verticalDistanceBetweenLines + shift 0189 } 0190 0191 NumberAnimation { 0192 id: noteAnimation 0193 target: musicElement 0194 properties: "x" 0195 duration: noteAnimationDuration 0196 from: multipleStaff.width - 10 0197 to: multipleStaff.clefImageWidth 0198 onStopped: { 0199 noteAnimationFinished() 0200 } 0201 } 0202 } 0203 } 0204 0205 Image { 0206 id: secondStaffDefaultClef 0207 sourceSize.width: musicElementModel.count ? multipleStaff.clefImageWidth : 0 0208 y: staves.count === 2 ? flickableTopMargin + staves.itemAt(1).y : 0 0209 visible: (currentEnteringStaff === 0) && (nbStaves === 2) 0210 source: background.clefType ? "qrc:/gcompris/src/activities/piano_composition/resource/" + background.clefType.toLowerCase() + "Clef.svg" 0211 : "" 0212 } 0213 } 0214 0215 Rectangle { 0216 id: pulseMarker 0217 width: activity.horizontalLayout ? 5 : 3 0218 border.width: width / 2 0219 height: staves.itemAt(0) == undefined ? 0 : 4 * staves.itemAt(0).verticalDistanceBetweenLines + width 0220 opacity: isPulseMarkerDisplayed && pulseMarkerAnimation.running 0221 color: "red" 0222 y: flickableTopMargin 0223 0224 property real nextPosition: 0 0225 0226 NumberAnimation { 0227 id: pulseMarkerAnimation 0228 target: pulseMarker 0229 property: "x" 0230 to: pulseMarker.nextPosition 0231 onStarted: { 0232 if(pulseMarker.height == 0 && staves.count != 0) { 0233 pulseMarker.height = Qt.binding(function() {return 4 * staves.itemAt(0).verticalDistanceBetweenLines + pulseMarker.width;}) 0234 } 0235 } 0236 onStopped: { 0237 if(pulseMarker.x === multipleStaff.width) 0238 pulseMarkerAnimationFinished() 0239 else 0240 playDrumSound() 0241 } 0242 } 0243 } 0244 0245 /** 0246 * Initializes the default clefs on the staves. 0247 * 0248 * @param clefType: The clef type to be initialized. 0249 */ 0250 function initClefs(clefType) { 0251 musicElementModel.clear() 0252 musicElementModel.append({ "elementType_": "clef", "clefType_": clefType, "staffNb_": 0, "isDefaultClef_": true, 0253 "noteName_": "", "noteType_": "", "soundPitch_": clefType, 0254 "highlightWhenPlayed_": false }) 0255 } 0256 0257 /** 0258 * Pauses the sliding animation of the notes. 0259 */ 0260 function pauseNoteAnimation() { 0261 for(var i = 0; i < musicElementModel.count; i++) { 0262 if(musicElementRepeater.itemAt(i).noteAnimation.running) 0263 musicElementRepeater.itemAt(i).noteAnimation.pause() 0264 } 0265 } 0266 0267 function resumeNoteAnimation() { 0268 for(var i = 0; i < musicElementModel.count; i++) { 0269 musicElementRepeater.itemAt(i).noteAnimation.resume() 0270 } 0271 } 0272 0273 /** 0274 * Gets all the details of any note like note image, position on staff etc. from NoteNotations. 0275 */ 0276 function getNoteDetails(noteName, noteType, clefType) { 0277 var notesDetails = NoteNotations.get() 0278 var noteNotation 0279 if(noteType === "Rest") 0280 noteNotation = noteName + noteType 0281 else 0282 noteNotation = clefType + noteName 0283 0284 for(var i = 0; i < notesDetails.length; i++) { 0285 if(noteNotation === notesDetails[i].noteName) { 0286 return notesDetails[i] 0287 } 0288 } 0289 } 0290 0291 /** 0292 * Adds a note to the staff. 0293 */ 0294 function addMusicElement(elementType, noteName, noteType, highlightWhenPlayed, playAudio, clefType, soundPitch, isUnflicked) { 0295 if(soundPitch == undefined || soundPitch === "") 0296 soundPitch = clefType 0297 0298 var isNextStaff = (selectedIndex == -1) && musicElementModel.count && ((staves.itemAt(0).width - musicElementRepeater.itemAt(musicElementModel.count - 1).x - musicElementRepeater.itemAt(musicElementModel.count - 1).width) < multipleStaff.clefImageWidth) 0299 0300 // 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. 0301 if(elementType === "clef" && musicElementModel.count && (selectedIndex == -1)) { 0302 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) 0303 isNextStaff = true 0304 } 0305 0306 if(isNextStaff && !noteAnimationEnabled) { 0307 multipleStaff.currentEnteringStaff++ 0308 if(multipleStaff.currentEnteringStaff >= multipleStaff.nbStaves) 0309 multipleStaff.nbStaves++ 0310 // When a new staff is added, initialise it with a default clef. 0311 musicElementModel.append({"noteName_": "", "noteType_": "", "soundPitch_": soundPitch, 0312 "clefType_": clefType, "highlightWhenPlayed_": false, 0313 "staffNb_": multipleStaff.currentEnteringStaff, 0314 "isDefaultClef_": true, "elementType_": "clef"}) 0315 0316 if(!isUnflicked) 0317 flickableStaves.flick(0, - nbStaves * multipleStaff.height) 0318 0319 if(elementType === "clef") 0320 return 0 0321 0322 isNextStaff = false 0323 } 0324 0325 if(selectedIndex === -1) { 0326 var isDefaultClef = false 0327 if(!musicElementModel.count) 0328 isDefaultClef = true 0329 musicElementModel.append({"noteName_": noteName, "noteType_": noteType, "soundPitch_": soundPitch, 0330 "clefType_": clefType, "highlightWhenPlayed_": highlightWhenPlayed, 0331 "staffNb_": multipleStaff.currentEnteringStaff, 0332 "isDefaultClef_": isDefaultClef, "elementType_": elementType}) 0333 0334 } 0335 else { 0336 var tempModel = createNotesBackup() 0337 var insertingIndex = selectedIndex + 1 0338 if(elementType === "clef") 0339 insertingIndex-- 0340 0341 tempModel.splice(insertingIndex, 0, {"elementType_": elementType, "noteName_": noteName, "noteType_": noteType, 0342 "soundPitch_": soundPitch, "clefType_": clefType }) 0343 if(elementType === "clef") { 0344 for(var i = 0; i < musicElementModel.count && tempModel[i]["elementType_"] != "clef"; i++) 0345 tempModel[i]["soundPitch_"] = clefType 0346 } 0347 selectedIndex = -1 0348 0349 redraw(tempModel) 0350 } 0351 0352 multipleStaff.selectedIndex = -1 0353 background.clefType = musicElementModel.get(musicElementModel.count - 1).soundPitch_ 0354 0355 if(playAudio) 0356 playNoteAudio(noteName, noteType, soundPitch, musicElementRepeater.itemAt(musicElementModel.count - 1).duration) 0357 } 0358 0359 /** 0360 * Creates a backup of the musicElementModel before erasing it. 0361 * 0362 * This backup data is used to redraw the notes. 0363 */ 0364 function createNotesBackup() { 0365 var tempModel = [] 0366 for(var i = 0; i < musicElementModel.count; i++) 0367 tempModel.push(JSON.parse(JSON.stringify(musicElementModel.get(i)))) 0368 0369 return tempModel 0370 } 0371 0372 /** 0373 * Redraws all the notes on the staves. 0374 */ 0375 function redraw(notes) { 0376 musicElementModel.clear() 0377 currentEnteringStaff = 0 0378 selectedIndex = -1 0379 for(var i = 0; i < notes.length; i++) { 0380 var note = notes[i] 0381 // On load melody from file, the first "note" is the BPM value 0382 if(note.bpm) { 0383 bpmValue = note.bpm; 0384 } 0385 else { 0386 addMusicElement(note["elementType_"], note["noteName_"], note["noteType_"], false, false, note["clefType_"], note["soundPitch_"], true) 0387 } 0388 } 0389 0390 // Remove the remaining unused staffs. 0391 if((multipleStaff.currentEnteringStaff + 1 < multipleStaff.nbStaves) && (multipleStaff.nbStaves > 2)) { 0392 nbStaves = multipleStaff.currentEnteringStaff + 1 0393 flickableStaves.flick(0, - nbStaves * multipleStaff.height) 0394 } 0395 0396 var lastMusicElement = musicElementModel.get(musicElementModel.count - 1) 0397 if(lastMusicElement.isDefaultClef_ && nbStaves > 2) { 0398 musicElementModel.remove(musicElementModel.count - 1) 0399 lastMusicElement = musicElementModel.get(musicElementModel.count - 1) 0400 } 0401 0402 if(lastMusicElement.staffNb_ < nbStaves - 1 && nbStaves != 2) 0403 nbStaves = lastMusicElement.staffNb_ + 1 0404 0405 currentEnteringStaff = lastMusicElement.staffNb_ 0406 background.clefType = lastMusicElement.soundPitch_ 0407 } 0408 0409 /** 0410 * Erases the selected note. 0411 * 0412 * @param noteIndex: index of the note to be erased 0413 */ 0414 function eraseNote(noteIndex) { 0415 musicElementModel.remove(noteIndex) 0416 selectedIndex = -1 0417 var tempModel = createNotesBackup() 0418 redraw(tempModel) 0419 } 0420 0421 /** 0422 * Erases all the notes. 0423 */ 0424 function eraseAllNotes() { 0425 musicElementModel.clear() 0426 selectedIndex = -1 0427 multipleStaff.currentEnteringStaff = 0 0428 initClefs(background.clefType) 0429 } 0430 0431 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} 0432 /** 0433 * Plays audio for a note. 0434 * 0435 * @param noteName: name of the note to be played. 0436 * @param noteType: note type to be played. 0437 */ 0438 function playNoteAudio(noteName, noteType, soundPitch, duration) { 0439 if(noteName) { 0440 if(noteType != "Rest") { 0441 // We should find a corresponding b type enharmonic notation for # type note to play the audio. 0442 if(noteName[1] === "#") { 0443 var blackKeysFlat = piano.blackKeyFlatNoteLabelsArray 0444 var blackKeysSharp = piano.blackKeySharpNoteLabelsArray 0445 0446 for(var i = 0; i < blackKeysSharp.length; i++) { 0447 if(blackKeysSharp[i][0] === noteName) { 0448 noteName = blackKeysFlat[i][0] 0449 break 0450 } 0451 } 0452 } 0453 0454 var octaveNb = "" 0455 var noteCharName = "" 0456 if(noteName[1] == "#" || noteName[1] == "b") { 0457 noteCharName = noteName[0] + noteName[1] 0458 octaveNb = noteName[2] 0459 } 0460 else 0461 { 0462 noteCharName = noteName[0] 0463 octaveNb = noteName[1] 0464 } 0465 var noteMidiName = (octaveNb-1)*12 + octave1MidiNumbersTable[noteCharName]; 0466 0467 GSynth.generate(noteMidiName, duration) 0468 } 0469 } 0470 } 0471 0472 /** 0473 * Get all the notes from the musicElementModel and returns the melody. 0474 */ 0475 function getAllNotes() { 0476 var notes = createNotesBackup() 0477 return notes 0478 } 0479 0480 /** 0481 * Loads melody from the provided data, to the staffs. 0482 * 0483 * @param data: melody to be loaded 0484 */ 0485 function loadFromData(data) { 0486 if(data != undefined) { 0487 var melody = data.split(" ") 0488 background.clefType = melody[0] 0489 eraseAllNotes() 0490 for(var i = 1 ; i < melody.length; ++i) { 0491 var noteLength = melody[i].length 0492 var noteName = melody[i][0] 0493 var noteType 0494 if(melody[i].substring(noteLength - 4, noteLength) === "Rest") { 0495 noteName = melody[i].substring(0, noteLength - 4) 0496 noteType = "Rest" 0497 } 0498 else if(melody[i][1] === "#" || melody[i][1] === "b") { 0499 noteType = melody[i].substring(3, melody[i].length) 0500 noteName += melody[i][1] + melody[i][2]; 0501 } 0502 else { 0503 noteType = melody[i].substring(2, melody[i].length) 0504 noteName += melody[i][1] 0505 } 0506 addMusicElement("note", noteName, noteType, false, false, melody[0]) 0507 } 0508 var tempModel = createNotesBackup() 0509 redraw(tempModel) 0510 } 0511 } 0512 0513 /** 0514 * Used in the activity play_piano. 0515 * 0516 * Checks if the answered note is correct 0517 */ 0518 function indicateAnsweredNote(isCorrectAnswer, noteIndexAnswered) { 0519 musicElementRepeater.itemAt(noteIndexAnswered).noteAnswered = true 0520 musicElementRepeater.itemAt(noteIndexAnswered).isCorrectlyAnswered = isCorrectAnswer 0521 } 0522 0523 /** 0524 * Used in the activity play_piano. 0525 * 0526 * Reverts the previous answer. 0527 */ 0528 function revertAnswer(noteIndexReverting) { 0529 musicElementRepeater.itemAt(noteIndexReverting).noteAnswered = false 0530 } 0531 0532 function play() { 0533 musicTimer.currentNote = 0 0534 selectedIndex = -1 0535 musicTimer.interval = 1 0536 if(isFlickable) 0537 flickableStaves.flick(0, nbStaves * multipleStaff.height) 0538 0539 pulseMarkerAnimation.stop() 0540 0541 if(musicElementModel.count > 1) 0542 pulseMarker.x = musicElementRepeater.itemAt(1).x + musicElementRepeater.itemAt(1).width / 2 0543 else 0544 pulseMarker.x = 0 0545 0546 musicTimer.start() 0547 } 0548 0549 function stopPlaying() { 0550 musicTimer.currentNote = -1 0551 musicTimer.stop() 0552 } 0553 0554 /** 0555 * Stops the audios playing. 0556 */ 0557 function stopAudios() { 0558 musicElementModel.clear() 0559 musicTimer.stop() 0560 items.audioEffects.stop() 0561 } 0562 0563 Timer { 0564 id: musicTimer 0565 0566 property int currentNote: 0 0567 0568 onRunningChanged: { 0569 if(!running && musicElementModel.get(currentNote) !== undefined) { 0570 var currentElement = musicElementModel.get(currentNote) 0571 var currentType = currentElement.noteType_ 0572 var note = currentElement.noteName_ 0573 var soundPitch = currentElement.soundPitch_ 0574 var currentStaff = currentElement.staffNb_ 0575 background.clefType = currentElement.clefType_ 0576 0577 if(currentElement.isDefaultClef_ && currentStaff > 1) { 0578 flickableStaves.contentY = staves.itemAt(currentStaff - 1).y 0579 } 0580 0581 musicTimer.interval = musicElementRepeater.itemAt(currentNote).duration 0582 if(multipleStaff.enableNotesSound) 0583 playNoteAudio(note, currentType, soundPitch, musicTimer.interval) 0584 pulseMarkerAnimation.stop() 0585 pulseMarkerAnimation.duration = Math.max(1, musicTimer.interval) 0586 if(musicElementRepeater.itemAt(currentNote + 1) != undefined) 0587 pulseMarker.nextPosition = musicElementRepeater.itemAt(currentNote + 1).x + musicElementRepeater.itemAt(currentNote + 1).width / 2 0588 else 0589 pulseMarker.nextPosition = multipleStaff.width 0590 0591 pulseMarkerAnimation.start() 0592 0593 if(!isPulseMarkerDisplayed) 0594 musicElementRepeater.itemAt(currentNote).highlightNote() 0595 currentNote++ 0596 musicTimer.start() 0597 } 0598 } 0599 } 0600 }