IOS sound from QML (AVAudioPlayer)

From Qt Wiki
Jump to navigation Jump to search

There is a couple of articles here, which allegedly contain instructions on how to get sound working on iOS via standard Qt classes QMediaPlayer and QML Audio element. I tried all of them and failed. Maybe they worked on earlier versions of iOS or Xcode, but not on iOS 8. So I was forced to write native code for the sound part of my app.

Bundle audio files in application

You can either play *.mp3 or *.aac files in iOS. Unfortunately *.ogg files won't work. I chose *.aac, because it appeared to have lower latency than mp3, which was important in my app (game). You can't use Qt resource system, instead, you have to include the audio files in your *.pro:

ios: {
    BUNDLE_DATA.files = $$files($$PWD/ios/*.aac)
    QMAKE_BUNDLE_DATA += BUNDLE_DATA

    HEADERS += CAudio.h
    OBJECTIVE_SOURCES += CAudio.mm
}

Make sure that your source code directory contains directory "ios" (on the same level as the *.pro), which contains your audio files. The second part of the snippet ensures that Objective-C++ source doesn't produce compile erros on other platforms.

The audio class itself

I wrote my audio class to substitute QML Audio element, so it supports properties source and muted.

File CAudio.h:

#pragma once

#include <QObject>

class CAudio : public QObject
{
	Q_OBJECT

	Q_PROPERTY(QString source READ source WRITE setSource)
	Q_PROPERTY(bool muted READ muted WRITE setMuted)

public:
	CAudio(QObject *parent = 0) : QObject(parent) {}

	QString source() const { return m_source; }
	void setSource(const QString &value) { m_source = value; }
	bool muted() const { return m_muted; }
	void setMuted(const bool &value) { m_muted = value; }

	Q_INVOKABLE void play() const;

private:
	QString m_source;
	bool m_muted;
};

The only method that is defined in *.mm file is play():

CAudio.mm:

#include "CAudio.h"
#import <AVFoundation/AVFoundation.h>
#include <QDebug>

void CAudio::play() const
{
	if(m_muted)
		return;

	QStringList splitPath = m_source.split("/");	// extract only filename from the path
	if(splitPath.length() < 2)
		return;
	QString fileName = splitPath.at(1);
	if(fileName.length() < 4)
		return;
	fileName.chop(4);		// remove file extension
	fileName += ".aac";

	NSString *soundFilePath = fileName.toNSString();
	NSURL *soundFileURL = [NSURL fileURLWithPath:soundFilePath];
	static AVAudioPlayer *audioPlayer = [AVAudioPlayer alloc];
	[audioPlayer initWithContentsOfURL:soundFileURL error:nil];
	[audioPlayer play];
}

On Android I'm playing files in Ogg vorbis format from Qt resource file. On iOS I prepared files in 'ios' directory with the same base name, but different extension. This code replaces the extension with ".aac". Note: The Objective-C part of the above code probably leaks memory. It appears to crash my application after ca. 30 minutes of game play. I attempted to eliminate the memory leak by making audioPlayer static, but the problem persists. You are welcome to submit corrections to this code.

The QML part

I'm registering my class only in iOS. This ensures that on other platforms the standard QML Audio element is used.

main.cpp:

#ifdef Q_OS_IOS
	qmlRegisterType<CAudio>("ios_audio", 1, 0, "Audio");
#else
	qmlRegisterType<QObject>("ios_audio", 1, 0, "DummyObject");		// import nothing if not in iOS
#endif

Finally, we can write cross-platform QML code like this:

import QtMultimedia 5.4
import ios_audio 1.0

...
    Audio {
        id: click
        source: "resources/click-1-off.ogg"
	muted: !settings.soundsEnabled
    }