QML

Moving on, we are done with the C++ side of things and can now look at the QML UI.

First, here is the main QML file:

import QtQuick 2.0 
import QtQuick.Scene3D 2.0 
import QtQuick.Layouts 1.2 
import QtMultimedia 5.0 
 
Item { 
    id: mainview 
    width: 1215 
    height: 720 
    visible: true 
    property bool isHoverEnabled: false 
    property int mediaLatencyOffset: 68 

The QML file consists out of a hierarchy of elements. Here, we define the top element, giving it its dimensions and name:

    state: "stopped" 
    states: [ 
        State { 
            name: "playing" 
            PropertyChanges { 
                target: playButtonImage 
                source: { 
                    if (playButtonMouseArea.containsMouse) 
                        "qrc:/images/pausehoverpressed.png" 
                    else 
                        "qrc:/images/pausenormal.png" 
                } 
            } 
            PropertyChanges { 
                target: stopButtonImage 
                source: "qrc:/images/stopnormal.png" 
            } 
        }, 
        State { 
            name: "paused" 
            PropertyChanges { 
                target: playButtonImage 
                source: { 
                    if (playButtonMouseArea.containsMouse) 
                        "qrc:/images/playhoverpressed.png" 
                    else 
                        "qrc:/images/playnormal.png" 
                } 
            } 
            PropertyChanges { 
                target: stopButtonImage 
                source: "qrc:/images/stopnormal.png" 
            } 
        }, 
        State { 
            name: "stopped" 
            PropertyChanges { 
                target: playButtonImage 
                source: "qrc:/images/playnormal.png" 
            } 
            PropertyChanges { 
                target: stopButtonImage 
                source: "qrc:/images/stopdisabled.png" 
            } 
        } 
    ]    

A number of states for the UI are defined, along with the changes that should be triggered if the state should change to it:

    Connections { 
        target: qmlinterface 
        onStopped: mainview.state = "stopped" 
        onPaused: mainview.state = "paused" 
        onPlaying: mainview.state = "started" 
        onStart: visualizer.startVisualization() 
    } 

These are the connections that link the signals from the C++ side to a local handler. We target our custom class as the source of these signals, then define the handler for each signal we wish to handle by prefixing it and adding the code that should be executed.

Here, we see that the start signal is linked to a handler that triggers the function in the visualization module that starts that module:

    Component.onCompleted: isHoverEnabled = qmlinterface.isHoverEnabled() 
 
    Image { 
        id: coverImage 
        anchors.fill: parent 
        source: "qrc:/images/albumcover.png" 
    } 

This Image element defines the background image, which we load from the resources that were added to the executable when the project was built:

    Scene3D { 
        anchors.fill: parent 
 
        Visualizer { 
            id: visualizer 
            animationState: mainview.state 
            numberOfBars: 120 
            barRotationTimeMs: 8160 // 68 ms per bar 
        } 
    } 

The 3D scene that will be filled with the visualizer's content is defined:

    Rectangle { 
        id: blackBottomRect 
        color: "black" 
        width: parent.width 
        height: 0.14 * mainview.height 
        anchors.bottom: parent.bottom 
    } 
 
    Text { 
        text: qmlinterface.durationTotal 
        color: "#80C342" 
        x: parent.width / 6 
        y: mainview.height - mainview.height / 8 
        font.pixelSize: 12 
    } 
 
    Text { 
        text: qmlinterface.durationLeft 
        color: "#80C342" 
        x: parent.width - parent.width / 6 
        y: mainview.height - mainview.height / 8 
        font.pixelSize: 12 
    } 

These two text elements are linked with the property in our custom C++ class, as we saw earlier. These values will be kept updated with the value in the C++ class instance as it changes:

    property int buttonHorizontalMargin: 10 
    Rectangle { 
        id: playButton 
        height: 54 
        width: 54 
        anchors.bottom: parent.bottom 
        anchors.bottomMargin: width 
        x: parent.width / 2 - width - buttonHorizontalMargin 
        color: "transparent" 
 
        Image { 
            id: playButtonImage 
            source: "qrc:/images/pausenormal.png" 
        } 
 
        MouseArea { 
            id: playButtonMouseArea 
            anchors.fill: parent 
            hoverEnabled: isHoverEnabled 
            onClicked: { 
                if (mainview.state == 'paused' || mainview.state == 'stopped') 
                    mainview.state = 'playing' 
                else 
                    mainview.state = 'paused' 
            } 
            onEntered: { 
                if (mainview.state == 'playing') 
                    playButtonImage.source = "qrc:/images/pausehoverpressed.png" 
                else 
                    playButtonImage.source = "qrc:/images/playhoverpressed.png" 
            } 
            onExited: { 
                if (mainview.state == 'playing') 
                    playButtonImage.source = "qrc:/images/pausenormal.png" 
                else 
                    playButtonImage.source = "qrc:/images/playnormal.png" 
            } 
        } 
    } 
 
    Rectangle { 
        id: stopButton 
        height: 54 
        width: 54 
        anchors.bottom: parent.bottom 
        anchors.bottomMargin: width 
        x: parent.width / 2 + buttonHorizontalMargin 
        color: "transparent" 
 
        Image { 
            id: stopButtonImage 
            source: "qrc:/images/stopnormal.png" 
        } 
 
        MouseArea { 
            anchors.fill: parent 
            hoverEnabled: isHoverEnabled 
            onClicked: mainview.state = 'stopped' 
            onEntered: { 
                if (mainview.state != 'stopped') 
                    stopButtonImage.source = "qrc:/images/stophoverpressed.png" 
            } 
            onExited: { 
                if (mainview.state != 'stopped') 
                    stopButtonImage.source = "qrc:/images/stopnormal.png" 
            } 
        } 
    } 
} 

The rest of the source serves to set up the individual buttons for controlling the playback, with play, stop, and pause buttons, which get swapped over as needed.

Next, we will look at the amplitude bar file:

import Qt3D.Core 2.0 
import Qt3D.Render 2.0 
import Qt3D.Extras 2.0 
import QtQuick 2.4 as QQ2 
 
Entity { 
    property int rotationTimeMs: 0 
    property int entityIndex: 0 
    property int entityCount: 0 
    property int startAngle: 0 + 360 / entityCount * entityIndex 
    property bool needsNewMagnitude: true 
    property real magnitude: 0 
    property real animWeight: 0 
 
    property color lowColor: "black" 
    property color highColor: "#b3b3b3" 
    property color barColor: lowColor 
 
    property string entityAnimationsState: "stopped" 
    property bool entityAnimationsPlaying: true 
 
    property var entityMesh: null 

A number of properties are defined before we dive into the animation state change handler:

    onEntityAnimationsStateChanged: { 
        if (animationState == "paused") { 
            if (angleAnimation.running) 
                angleAnimation.pause() 
            if (barColorAnimations.running) 
                barColorAnimations.pause() 
        } else if (animationState == "playing"){ 
            needsNewMagnitude = true; 
            if (heightDecreaseAnimation.running) 
                heightDecreaseAnimation.stop() 
            if (angleAnimation.paused) { 
                angleAnimation.resume() 
            } else if (!entityAnimationsPlaying) { 
                magnitude = 0 
                angleAnimation.start() 
                entityAnimationsPlaying = true 
            } 
            if (barColorAnimations.paused) 
                barColorAnimations.resume() 
        } else { 
            if (animWeight != 0) 
                heightDecreaseAnimation.start() 
            needsNewMagnitude = true 
            angleAnimation.stop() 
            barColorAnimations.stop() 
            entityAnimationsPlaying = false 
        } 
    } 

Every time the audio playback is stopped, paused, or started, the animation has to be updated to match this state change:

    property Material barMaterial: PhongMaterial { 
        diffuse: barColor 
        ambient: Qt.darker(barColor) 
        specular: "black" 
        shininess: 1 
    } 

This defines the look of the amplitude bars, using Phong shading:

    property Transform angleTransform: Transform { 
        property real heightIncrease: magnitude * animWeight 
        property real barAngle: startAngle 
 
        matrix: { 
            var m = Qt.matrix4x4() 
            m.rotate(barAngle, Qt.vector3d(0, 1, 0)) 
            m.translate(Qt.vector3d(1.1, heightIncrease / 2 - heightIncrease * 0.05, 0)) 
            m.scale(Qt.vector3d(0.5, heightIncrease * 15, 0.5)) 
            return m; 
        } 
 
        property real compareAngle: barAngle 
        onBarAngleChanged: { 
            compareAngle = barAngle 
 
            if (compareAngle > 360) 
                compareAngle = barAngle - 360 
 
            if (compareAngle > 180) { 
                parent.enabled = false 
                animWeight = 0 
                if (needsNewMagnitude) { 
                    // Calculate the ms offset where the bar will be at the center point of the 
                    // visualization and fetch the correct magnitude for that point in time. 
                    var offset = (90.0 + 360.0 - compareAngle) * (rotationTimeMs / 360.0) 
                    magnitude = qmlinterface.getNextAudioLevel(offset) 
                    needsNewMagnitude = false 
                } 
            } else { 
                parent.enabled = true 
                // Calculate a power of 2 curve for the bar animation that peaks at 90 degrees 
                animWeight = Math.min((compareAngle / 90), (180 - compareAngle) / 90) 
                animWeight = animWeight * animWeight 
                if (!needsNewMagnitude) { 
                    needsNewMagnitude = true 
                    barColorAnimations.start() 
                } 
            } 
        } 
    } 

As the amplitude bars move across the screen, they change relative to the camera, so we need to keep calculating the new angle and display height.

In this section, we also replaced the original call to the audio level method with a call to the new method in our C++ class:

    components: [entityMesh, barMaterial, angleTransform] 
 
    QQ2.NumberAnimation { 
        id: angleAnimation 
        target: angleTransform 
        property: "barAngle" 
        duration: rotationTimeMs 
        loops: QQ2.Animation.Infinite 
        running: true 
        from: startAngle 
        to: 360 + startAngle 
    } 
 
    QQ2.NumberAnimation { 
        id: heightDecreaseAnimation 
        target: angleTransform 
        property: "heightIncrease" 
        duration: 400 
        running: false 
        from: angleTransform.heightIncrease 
        to: 0 
        onStopped: barColor = lowColor 
    } 
 
    property int animationDuration: angleAnimation.duration / 6 
 
    QQ2.SequentialAnimation on barColor { 
        id: barColorAnimations 
        running: false 
 
        QQ2.ColorAnimation { 
            from: lowColor 
            to: highColor 
            duration: animationDuration 
        } 
 
        QQ2.PauseAnimation { 
            duration: animationDuration 
        } 
 
        QQ2.ColorAnimation { 
            from: highColor 
            to: lowColor 
            duration: animationDuration 
        } 
    } 
} 

The rest of the file contains a few more animation transforms.

Finally, here is the visualization module:

import Qt3D.Core 2.0 
import Qt3D.Render 2.0 
import Qt3D.Extras 2.0 
import QtQuick 2.2 as QQ2 
 
Entity { 
    id: sceneRoot 
    property int barRotationTimeMs: 1 
    property int numberOfBars: 1 
    property string animationState: "stopped" 
    property real titleStartAngle: 95 
    property real titleStopAngle: 5 
 
    onAnimationStateChanged: { 
        if (animationState == "playing") { 
            qmlinterface.setPlaying(); 
            if (progressTransformAnimation.paused) 
                progressTransformAnimation.resume() 
            else 
                progressTransformAnimation.start() 
        } else if (animationState == "paused") { 
            qmlinterface.setPaused(); 
            if (progressTransformAnimation.running) 
                progressTransformAnimation.pause() 
        } else { 
            qmlinterface.setStopped(); 
            progressTransformAnimation.stop() 
            progressTransform.progressAngle = progressTransform.defaultStartAngle 
        } 
    } 

This section got changed from interacting with the local media player instance to the new one in the C++ code. Beyond that, we left it unchanged. This is the main handler for when anything changes in the scene due to user interaction, or a track starting or ending:

    QQ2.Item { 
        id: stateItem 
 
        state: animationState 
        states: [ 
            QQ2.State { 
                name: "playing" 
                QQ2.PropertyChanges { 
                    target: titlePrism 
                    titleAngle: titleStopAngle 
                } 
            }, 
            QQ2.State { 
                name: "paused" 
                QQ2.PropertyChanges { 
                    target: titlePrism 
                    titleAngle: titleStopAngle 
                } 
            }, 
            QQ2.State { 
                name: "stopped" 
                QQ2.PropertyChanges { 
                    target: titlePrism 
                    titleAngle: titleStartAngle 
                } 
            } 
        ] 
 
        transitions: QQ2.Transition { 
            QQ2.NumberAnimation { 
                property: "titleAngle" 
                duration: 2000 
                running: false 
            } 
        } 
    } 

A number of property changes and transitions are defined for the track title object:

    function startVisualization() { 
        progressTransformAnimation.duration = qmlinterface.duration() 
        mainview.state = "playing" 
        progressTransformAnimation.start() 
    } 

This function is what starts the entire visualization sequence. It uses the track duration, as obtained via our C++ class instance, to determine the dimensions of the progress bar for the track playback animation before starting the visualization animation:

    Camera { 
        id: camera 
        projectionType: CameraLens.PerspectiveProjection 
        fieldOfView: 45 
        aspectRatio: 1820 / 1080 
        nearPlane: 0.1 
        farPlane: 1000.0 
        position: Qt.vector3d(0.014, 0.956, 2.178) 
        upVector: Qt.vector3d(0.0, 1.0, 0.0) 
        viewCenter: Qt.vector3d(0.0, 0.7, 0.0) 
    } 

A camera is defined for the 3D scene:

    Entity { 
        components: [ 
            DirectionalLight { 
                intensity: 0.9 
                worldDirection: Qt.vector3d(0, 0.6, -1) 
            } 
        ] 
    } 
 
    RenderSettings { 
        id: external_forward_renderer 
        activeFrameGraph: ForwardRenderer { 
            camera: camera 
            clearColor: "transparent" 
        } 
    } 

A renderer and light for the scene are created:

    components: [external_forward_renderer] 
 
    CuboidMesh { 
        id: barMesh 
        xExtent: 0.1 
        yExtent: 0.1 
        zExtent: 0.1 
    } 

A mesh is created for the amplitude bars:

    NodeInstantiator { 
        id: collection 
        property int maxCount: parent.numberOfBars 
        model: maxCount 
 
        delegate: BarEntity { 
            id: cubicEntity 
            entityMesh: barMesh 
            rotationTimeMs: sceneRoot.barRotationTimeMs 
            entityIndex: index 
            entityCount: sceneRoot.numberOfBars 
            entityAnimationsState: animationState 
            magnitude: 0 
        } 
    } 

The number of bars, along with other properties, is defined:

    Entity { 
        id: titlePrism 
        property real titleAngle: titleStartAngle 
 
        Entity { 
            id: titlePlane 
 
            PlaneMesh { 
                id: titlePlaneMesh 
                width: 550 
                height: 100 
            } 
 
            Transform { 
                id: titlePlaneTransform 
                scale: 0.003 
                translation: Qt.vector3d(0, 0.11, 0) 
            } 
 
            NormalDiffuseMapAlphaMaterial { 
                id: titlePlaneMaterial 
                diffuse: TextureLoader { source: "qrc:/images/demotitle.png" } 
                normal: TextureLoader { source: "qrc:/images/normalmap.png" } 
                shininess: 1.0 
            } 
 
            components: [titlePlaneMesh, titlePlaneMaterial, titlePlaneTransform] 
        } 

This plane contains the title object whenever there's no track playing:

        Entity { 
            id: songTitlePlane 
 
            PlaneMesh { 
                id: songPlaneMesh 
                width: 550 
                height: 100 
            } 
 
            Transform { 
                id: songPlaneTransform 
                scale: 0.003 
                rotationX: 90 
                translation: Qt.vector3d(0, -0.03, 0.13) 
            } 
 
            property Material songPlaneMaterial: NormalDiffuseMapAlphaMaterial { 
                diffuse: TextureLoader { source: "qrc:/images/songtitle.png" } 
                normal: TextureLoader { source: "qrc:/images/normalmap.png" } 
                shininess: 1.0 
            } 
 
            components: [songPlaneMesh, songPlaneMaterial, songPlaneTransform] 
        } 

This plane contains the song title whenever a track is active:

        property Transform titlePrismPlaneTransform: Transform { 
            matrix: { 
                var m = Qt.matrix4x4() 
                m.translate(Qt.vector3d(-0.5, 1.3, -0.4)) 
                m.rotate(titlePrism.titleAngle, Qt.vector3d(1, 0, 0)) 
                return m; 
            } 
        } 
 
        components: [titlePlane, songTitlePlane, titlePrismPlaneTransform] 
    } 

To transform the planes between playing and non-playing transitions, this transform is used:

    Mesh { 
        id: circleMesh 
        source: "qrc:/meshes/circle.obj" 
    } 
 
    Entity { 
        id: circleEntity 
        property Material circleMaterial: PhongAlphaMaterial { 
            alpha: 0.4 
            ambient: "black" 
            diffuse: "black" 
            specular: "black" 
            shininess: 10000 
        } 
 
        components: [circleMesh, circleMaterial] 
    } 

A circle mesh that provides a reflection effect is added:

    Mesh { 
        id: progressMesh 
        source: "qrc:/meshes/progressbar.obj" 
    } 
 
    Transform { 
        id: progressTransform 
        property real defaultStartAngle: -90 
        property real progressAngle: defaultStartAngle 
        rotationY: progressAngle 
    } 
 
    Entity { 
        property Material progressMaterial: PhongMaterial { 
            ambient: "purple" 
            diffuse: "white" 
        } 
 
        components: [progressMesh, progressMaterial, progressTransform] 
    } 
 
    QQ2.NumberAnimation { 
        id: progressTransformAnimation 
        target: progressTransform 
        property: "progressAngle" 
        duration: 0 
        running: false 
        from: progressTransform.defaultStartAngle 
        to: -270 
        onStopped: if (animationState != "stopped") animationState = "stopped" 
    } 
} 

Finally, this mesh creates the progress bar, which moves from the left to the right to indicate playback progress.

The entire project is compiled by running qmake followed by make, or by opening the project in Qt Creator and building it from there. When run, it will automatically start playing the included song and show the amplitude visualization, while being controllable via the buttons in the UI.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.232.187