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 }