Qt-based BB10 API Examples Documentation

Contents

Dictaphone Example

Files:

Description

The Dictaphone example simulates an analog dictaphone. It allows you to record some voice, pause the recording and play it back later on. You can also record multiple tracks in a sequence.

Overview

In this example we'll learn how to use the AudioRecorder and MediaPlayer classes of the BB10 framework to record some audio data and play them back afterwards. The recorded data are stored inside a file on the file system from where the MediaPlayer can load them.

The UI

The UI of this sample application consists of two pages. The main page contains some graphical accessoires like a speaker box, an LED and a tape cover to make it look like a real dictaphone, and the three buttons to interact with the application. Clicking the 'Play' button opens a separated page which lists all recorded tracks and allows the user to play back one of them.

    // The led object
    Led {
        id: led
        horizontalAlignment: HorizontalAlignment.Left
        verticalAlignment: VerticalAlignment.Top
        translationX: 40
        translationY: 500
        state: (recorder.mediaState == MediaState.Started ? "rec" : recorder.mediaState == MediaState.Paused ? "pause" : "off")
    }

The first accessoire is the LED object, which is implemented in the separated file Led.qml.

    ImageView {
        property string state: "off"

        imageSource: (state == "rec" ? "asset:///images/led_rec.png" :
                      state == "pause" ? "asset:///images/led_pause.png" :
                      "asset:///images/led_off.png")
    }

It consists of an ImageView that shows different images depending on the value of its custom property 'state'. The value of the 'state' property is bound inside main.qml against the current media state of the AudioRecorder object.

    // The tape object
    Tape {
        id: tape
        horizontalAlignment: HorizontalAlignment.Center
        verticalAlignment: VerticalAlignment.Top

        translationY: 570

        running: (recorder.mediaState == MediaState.Started)
    }

The second accessoire is the tape object, which is implemented in the file Tape.qml.

    Container {
        property bool running: false

        onRunningChanged: {
            if (running)
                animation.play()
            else
                animation.stop()
        }

        layout: DockLayout {}

        ImageView {
            horizontalAlignment: HorizontalAlignment.Center
            verticalAlignment: VerticalAlignment.Center

            imageSource: "asset:///images/tape_shadow.png"
        }

        ImageView {
            id: leftGear

            horizontalAlignment: HorizontalAlignment.Center
            verticalAlignment: VerticalAlignment.Center

            translationX: -155
            translationY: -8

            imageSource: "asset:///images/tape_gear.png"
        }

        ImageView {
            id: rightGear

            horizontalAlignment: HorizontalAlignment.Center
            verticalAlignment: VerticalAlignment.Center

            translationX: 138
            translationY: -8

            imageSource: "asset:///images/tape_gear.png"
        }

        ImageView {
            horizontalAlignment: HorizontalAlignment.Center
            verticalAlignment: VerticalAlignment.Center

            imageSource: "asset:///images/tape_cover.png"
        }

        animations: ParallelAnimation {
            id: animation

            SequentialAnimation {
                target: leftGear

                RotateTransition {
                    fromAngleZ: 0
                    toAngleZ: 1800
                    duration: 15000
                    easingCurve: StockCurve.Linear
                }
            }
            SequentialAnimation {
                target: rightGear

                RotateTransition {
                    fromAngleZ: 0
                    toAngleZ: 1800
                    duration: 15000
                    easingCurve: StockCurve.Linear
                }
            }
        }

It consists of a Container that contains a couple of ImageView objects inside a DockLayout, basically the background of the tape, the two tape gears and the tape cover. Since we want to animate the tape gears during a recording, we define two RotateTransitions on them inside a ParallelAnimation. This animation will be started or stopped depending on the value of the custom property 'running'. The value of the 'running' property is again bound inside main.qml against the current media state of the AudioRecorder object.

    // The 'Record' button
    ImageToggleButton {
        rightMargin: 2
        imageSourceDefault: "asset:///images/rec_button.png"
        imageSourcePressedUnchecked: "asset:///images/rec_button.png"
        imageSourceChecked: "asset:///images/rec_button_pressed.png"
        imageSourcePressedChecked: "asset:///images/rec_button_pressed.png"
        imageSourceDisabledChecked: "asset:///images/rec_button_pressed.png"
        enabled: (recorder.mediaState != MediaState.Paused)
        onCheckedChanged: {
            if (recorder.mediaState == MediaState.Started) {
                // Stop the recorder
                recorder.reset()

                // Update the internal track list
                _trackManager.update()

                // Play the finished sound
                recordStopSound.play()
            } else {
                // Update the internal track list
                _trackManager.update()

                // Configure the recorder to use a new URL
                recorder.outputUrl = "file://" + _trackManager.nextTrackUrl()

                // Play the start sound
                recordStartSound.play()

                // Start the recorder
                recorder.record()
            }
        }
    }

The 'Record' button is implemented as an ImageToggleButton with two different images for its normal and pressed state. Whenever the user toggles its checked state, we test for the current media state of the AudioRecorder and stop or start the recorder. If we start a new recording, we first initialize the 'outputUrl' of the recorder with a new file name that we get from the exported TrackManager object. Additionally we play an appropriated SystemSound.

    // The 'Pause' button
    ImageToggleButton {
        leftMargin: 2
        rightMargin: 2
        imageSourceDefault: "asset:///images/pause_button.png"
        imageSourceDisabledUnchecked: "asset:///images/pause_button.png"
        imageSourcePressedUnchecked: "asset:///images/pause_button.png"
        imageSourceChecked: "asset:///images/pause_button_pressed.png"
        imageSourcePressedChecked: "asset:///images/pause_button_pressed.png"
        enabled: (recorder.mediaState == MediaState.Started || recorder.mediaState == MediaState.Paused)
        onCheckedChanged: {
            if (recorder.mediaState == MediaState.Started) recorder.pause(); else recorder.record()
        }
    }

The 'Pause' button is also implemented as an ImageToggleButton with two different images for its normal and pressed state. Whenever the user toggles its checked state, we test for the current media state of the AudioRecorder and pause or continue the recording.

    // The 'Play' button
    ImageButton {
        leftMargin: 2
        defaultImageSource: "asset:///images/play_button.png"
        pressedImageSource: "asset:///images/play_button_pressed.png"
        disabledImageSource: "asset:///images/play_button.png"
        enabled: (_trackManager.hasRecordedTracks && recorder.mediaState != MediaState.Started && recorder.mediaState != MediaState.Paused)
        onClicked: navigationPane.push(playerPage.createObject())
    }

The 'Play' button is implemented as a normal ImageButton since we don't want to have the toggle behavior. If the user clicks the button, we open the player page, which is implemented in PlayerPage.qml.

    attachedObjects: [
        AudioRecorder {
            id: recorder
        },
        ComponentDefinition {
            id: playerPage
            source: "PlayerPage.qml"
        },
        SystemSound {
            id: recordStartSound
            sound: SystemSound.RecordingStartEvent
        },
        SystemSound {
            id: recordStopSound
            sound: SystemSound.RecordingStopEvent
        },
        // application supports changing the Orientation
        OrientationHandler {
            // onOrientationChanged: { should be this from docs, but onOrientationAboutToChange runs smoother
            onOrientationAboutToChange: {
                mainPage.reLayout(orientation);
            }
        }
    ]

The AudioRecorder, PlayerPage and the two SystemSound objects are created as attached objects to the Page object.

    Page {
        Container {
            layout: DockLayout {}

            // The background image
            ImageView {
                horizontalAlignment: HorizontalAlignment.Fill
                verticalAlignment: VerticalAlignment.Fill

                imageSource: "asset:///images/sheet_background.png"
            }

            Container {
                horizontalAlignment: HorizontalAlignment.Fill
                verticalAlignment: VerticalAlignment.Fill

                leftPadding: 30
                topPadding: 30
                rightPadding: 30
                bottomPadding: 30

                // The title label
                Label {
                    horizontalAlignment: HorizontalAlignment.Center

                    text: qsTr ("Recorded Tracks")
                    textStyle {
                        base: SystemDefaults.TextStyles.BigText
                        color: Color.White
                    }
                }

                // The recorded tracks list view
                ListView {
                    id: listView

                    horizontalAlignment: HorizontalAlignment.Center
                    topMargin: 50

                    dataModel: _trackManager.model

                    listItemComponents: ListItemComponent {
                        type: "item"
                        StandardListItem {
                            title: ListItemData.name
                        }
                    }

                    onTriggered: {
                        clearSelection()
                        select(indexPath)
                    }
                }

                // The 'Play' button
                Button {
                    horizontalAlignment: HorizontalAlignment.Center
                    topMargin: 50

                    text: qsTr ("Play")

                    onClicked: {
                        // Reset URL of player
                        player.sourceUrl = ""

                        // Set the currently selected track as player source URL
                        player.sourceUrl = "file://" + listView.dataModel.data(listView.selected()).url

                        // Start playback
                        player.play()
                    }
                }
            }
        }

        actions: [
            ActionItem {
                title: qsTr ("Clear All Tracks")
                ActionBar.placement: ActionBarPlacement.OnBar
                imageSource: "asset:///images/delete.png"

                onTriggered: {
                    _trackManager.clearAllTracks()
                    navigationPane.pop()
                }
            }
        ]

        attachedObjects: [
            MediaPlayer {
                id: player
            }
        ]
    }

The PlayerPage contains a ListView that uses the TrackManager's model as data model and a Button to start the playback of the currently selected track. The actual playback is done by the MediaPlayer object, which is created as attached object of the page. There are also two actions to close the page and go back to the main screen or to delete all recorded tracks from the file system.

TrackManager

The TrackManager provides information about the recorded tracks to the UI. To make it accessible in the QML file we export it as context property inside the main function.

        // Load the UI description from main.qml
        QmlDocument *qml = QmlDocument::create("asset:///main.qml").parent(&app);

        // Make the TrackManager object available to the UI as context property
        qml->setContextProperty("_trackManager", new TrackManager(&app));

The first of the two properties of TrackManager is 'model', that provides a GroupDataModel filled with all the available tracks from the file system. The second property is 'hasRecordedTracks', which reports whether there are tracks available in the file system at all.

    class TrackManager : public QObject
    {
        Q_OBJECT

        // A model that contains a list of all recorded tracks
        Q_PROPERTY(bb::cascades::GroupDataModel* model READ model CONSTANT)

        // A flag that defines whether recorded tracks are available or not
        Q_PROPERTY(bool hasRecordedTracks READ hasRecordedTracks NOTIFY hasRecordedTracksChanged)

    public:
        TrackManager(QObject *parent = 0);

        // This method is invoked to get the target URL for the next track to record.
        Q_INVOKABLE QUrl nextTrackUrl() const;

    public Q_SLOTS:
        // This method is invoked to clear all recorded tracks
        void clearAllTracks();

        // This method is invoked to update the internal state of the track manager.
        void update();

    Q_SIGNALS:
        // The change notification signal of the property
        void hasRecordedTracksChanged();

    private:
        // A helper method to update internal data from the track storage location
        void updateTrackInformation();

        // The accessor methods of the properties
        bb::cascades::GroupDataModel* model() const;
        bool hasRecordedTracks() const;

        // The model that contains the list of recorded tracks
        bb::cascades::GroupDataModel *m_model;

        // The internal track counter
        unsigned int m_trackCounter;
    };

When the user starts a new recording, the nextTrackUrl() method is invoked on the TrackManager. This method increments its internal track counter and assembles and returns a new file URL.

    QUrl TrackManager::nextTrackUrl() const
    {
        // The next track will have the current track number incremented by one
        const int nextTrack = m_trackCounter + 1;

        // Return an URL in the form "app/native/tracks/track001.m4a"
        return QUrl(QString::fromLatin1("%1/track%2.m4a").arg(QDir(trackStorageLocation()).absolutePath())
                                                         .arg(nextTrack, 3, 10, QLatin1Char('0')));
    }

If the user triggers the 'Clear All Tracks' action in the player page, the clearAllTracks() method is invoked on the TrackManager. This method iterates over all files in the track storage directory and removes them.

    void TrackManager::clearAllTracks()
    {
        // Iterate over all files in the track directory and delete them
        QDirIterator it(trackStorageLocation(), QDir::Files | QDir::NoDotAndDotDot);
        while (it.hasNext()) {
            it.next();

            QFile::remove(it.fileInfo().absoluteFilePath());
        }

        updateTrackInformation();
    }

Whenever the number of recorded tracks has changed, the updateTrackInformation() method is invoked. Inside this method the model is cleared and the internal track counter is reset. Afterwards we iterate over the track storage directory and for each found file, we add an entry to the model and increase the track counter.

    void TrackManager::updateTrackInformation()
    {
        const bool oldHasRecordedTracks = hasRecordedTracks();

        // Clear the content of the tracks model
        m_model->clear();

        // Reset the internal track counter
        m_trackCounter = 0;

        // Iterate over the track storage directory and fill the model
        QDirIterator it(trackStorageLocation(), QDir::Files | QDir::NoDotAndDotDot);
        while (it.hasNext()) {
            it.next();

            QVariantMap entry;
            entry["name"] = it.fileName(); // used as title in the ListView
            entry["url"] = it.fileInfo().absoluteFilePath(); // used by the MediaPlayer

            m_model->insert(entry);

            m_trackCounter++;
        }

        // Emit change notification signal if the hasRecordedTracks property has changed
        if (oldHasRecordedTracks != hasRecordedTracks())
            emit hasRecordedTracksChanged();
    }