Files:
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.
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 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.
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(); }