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 }