Audio Manager Compliant App Writer's Guide
Introduction
Moblin Audio Manager is a core component of Moblin, and it is used to manage all audio apps/streams/devices of the system with regards to the configuration file. If an audio application is not compliant with AM, what AM can do with this kind of App is basically limited to volume control, mute or unmute, and for LPE applications, AM will not be able to control the LPE stream. If an audio application is compliant with AM, AM will be able to pause the application with the cooperation of the application, and AM will be able to control the LPE stream that is created by LPE application.
AM provides a set DBUS methods and signals for user to write AM compliant applications, please refer to “Moblin Audio Manager API”.
This doc is a guide for developers to write AM compliant applications with the API spec. Readers of this doc should have the pre-knowledge of Moblin Audio Manager API spec.
Pre-Knowledge
Do Not Use ALSA API
AM compliant applications should not use ALSA API directly, and for applications that use MMF like Helix/Gstreamer should not use the ALSA plug-in of the MMF. Instead, they should use PA API . The reason is as following: In Helix, if user choose to use ALSA audio device, from code perspective, it will use Class CAudioOutLinuxAlsa, and if user pauses Helix, the _Reset function will be called, and inside the _Reset function, snd_pcm_drop and snd_pcm_prepare will be invoked.
The problem occurs inside snd_pcm_prepare. Because ALSA is configured to use PA, then the snd_pcm_prepare will finally go into ALSA plug-in for PA, and call pulse_prepare which will first pa_stream_disconnect, and then pa_stream_new.
The result is that if Helix uses ALSA audio device, Helix will execute pause of a pa_stream as “destroy pa_stream then create new pa_stream”, the semantic is fundamentally wrong for AM. So does Gstreamer, if Gstreamer uses alsasink, the issue is the same.
But if Gstreamer uses pulsesink, the issue does not exist. The pause of pipeline will call gstaudiosink’s gst_audioringbuffer_pause which in turns calls the pulsesink’s gst_pulsesink_reset, and inside that reset function, there’s no pa_stream disconnect/new operation, it’s just some PA function calls to pause that pa_stream.
So, conclusion is if user wants to write an AM compliant app, do not use ALSA anymore, instead, use PA APIs. For non-compliant apps, no restriction.
PA Stream Creation Notes
Stream Name
As written in API spec, after the AM stream is created inside AM (as soon as the pcm data is written into the pa_stream), AM will emit a signal StreamCreated which contains 4 arguments: binary name of the application, stream name of the created stream, AM stream id of the created stream, type of the created stream. Then application is supposed to use the AM stream id to execute commands from AM.
Pay attention to the stream name and AM stream id pair. If application created multiple audio streams, there must be a way for application to know which AM stream id refers to which audio stream; that’s what stream name does. When application creates pa_stream with pa_stream_new, application could specify the name of the pa_stream(set the proplist of pa_stream, key is am.stream.name, value is the stream name user specifies), so that after application receives the StreamCreated signal, application could know which AM stream id refers to which pa_stream; hence application could know which pa_stream to operate on when receives commands from AM, e.g. pause a stream of stream id xx. If application only creates on audio stream, then stream name is not important.
If application uses MMF and wants to create multiple audio streams. For Gstreamer, which means creating multiple pipelines, then pulsesink may need to be added a property stream_name which is to specify the name of the pa_stream that is created inside pulsesink. For Helix, TODO.
Stream Property
There are two properties of pa_stream that need to be pay attention to. One is “am.media.role”, the other is “LPE”. Media role actually specifies the priority of the audio stream; LPE specifies whether the audio stream is a LPE Decode-N-Return stream or not.
If application wants to specify the priority of the audio stream that is going to be created, application could set the “am.media.role” property of the pa_stream when creating the pa_stream using API pa_stream_new_with_proplist. If application does not specify am.media.role property, the media role of the audio stream will be gotten from configuration file if it is configured there, or be set with a default value.
If LPE application receives signal StreamDisableLPEDirectRender, it will shift from Fire-N-Forget to Decode-N-Return mode, and will create a pa_stream to receive the decoded data, it SHOULD specify the “LPE” property of the pa_stream to “yes”, so that AM could know that this a LPE Decode-N-Return stream.
LPE Stream Creation Notes
It should be noted that the LPE stream registration LPEStreamRegister will trigger evaluation inside Audio Manager if it succeeds, and one possible consequence is that the LPE APP/MMF may receive a StreamMuted signal right after registration (StreamCreated signal will be received first of course). Will this have impact on MMF implementation?
When invoking LPEStreamRegister, user could specify the name of the LPE stream if user want to create multiple LPE streams, so that user could distinguish the AM stream id between different LPE streams. And user could also specify the media role of the LPE stream
Programming Guide
None-LPE Application
Single Stream
It is simple to write applications of single stream, and this is the most common case. To write applications of single stream, user doesn’t even need to specify the stream name of the pa_stream; and it’s optional to set the am.media.role property. If am.media.role property is not set, AM will try to use configure info from configuration file, if there’s no info inside configuration file, default value will be applied.

Multiple Streams
The difference between writing application of multiple streams and writing application of single stream is that developer should pay attention to the AM stream id binding to pa_stream.
LPE Application
Single Stream
Writing a LPE application should be very similar to writing a non-LPE application. From application perspective, what it creates is AM stream, not LPE stream or PA stream. Below work flows assume MMF will call LPEStreamRegister during initial phase.

Code snippet
The attached seek.c is modified from gst-plugins-base/tests/examples/seek.c, it is added with AM related code thus become AM compliant, you can put it under gst-plugins-base folder and compile.
///////////////////////////////////////////AM related starts///////////////////////////////////////////////////////////
static gchar* app_binary_name = "seek1"; /* This is simplified, this demo will not use pid to query its binary name, just use fixed binary name for test*/
static guint32 app_playback_stream_id = -1; /* This demo is simplified, only one playback stream is demonstrated */
static gboolean am_mute = FALSE; /* This parameter indicates AM has muted the audio */
static gboolean am_pause = FALSE; /* This parameter indicates app is paused in response to AM */
static gboolean app_video = TRUE; /* whether audio stream accompanies with video */
static gboolean app_can_resume = TRUE; /* whether user should be able to resume */
static gboolean app_paused = TRUE; /* whether audio is paused or not */
static gboolean initial_state = TRUE; /* Indicate whether the pipeline is in initial state, if so, no need to notify resume */
static void do_resume();
static void do_pause();
DBusGConnection *connection;
GError *error;
DBusGProxy *proxy = NULL;
/* Connect to am dbus server
* return 0 means failed
* return 1 means succeeded
* */
gint dbus_init() {
char *name = "org.moblin.audiomanager";
char *path = "/org/moblin/audiomanager";
char *interface = "org.moblin.audiomanager";
connection = dbus_g_bus_get(DBUS_BUS_SESSION, &error);
if (connection == NULL) {
g_printerr("Failed to open connection to bus: %s\n",
error->message);
g_error_free(error);
return 0;
}
proxy = dbus_g_proxy_new_for_name(connection, name,
path, interface);
if (proxy == NULL) {
g_print("connect to audio manager dbus server failed\n");
return 0;
}
else {
g_print("connect to audio manager dbus server succeeded\n");
}
return 1;
}
static void update_pause_button(gboolean enable)
{
gtk_widget_set_sensitive(pause_button, enable);
}
static void update_resume_button(gboolean enable)
{
gtk_widget_set_sensitive(play_button, enable);
}
static void do_resume()
{
GstStateChangeReturn ret;
if (state != GST_STATE_PLAYING) {
g_print ("PLAY pipeline\n");
ret = gst_element_set_state (pipeline, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE)
goto failed;
//do_seek(hscale);
state = GST_STATE_PLAYING;
}
return;
failed:
{
g_print ("PLAY failed\n");
}
}
static void
play_cb (GtkButton * button, gpointer data)
{
if (initial_state)
{
do_resume();
app_paused = FALSE;
am_pause = FALSE;
update_pause_button(!app_paused);
update_resume_button(app_paused);
initial_state = FALSE;
return;
}
if (app_can_resume)
{
if (app_playback_stream_id == -1)
g_warning("app have no playback stream\n");
am_stream_notify_resume(app_playback_stream_id, FALSE);
do_resume();
app_paused = FALSE;
am_pause = FALSE;
update_pause_button(!app_paused);
update_resume_button(app_paused);
}
}
static void do_pause()
{
if (state != GST_STATE_PAUSED) {
GstStateChangeReturn ret;
g_print ("PAUSE pipeline\n");
ret = gst_element_set_state (pipeline, GST_STATE_PAUSED);
if (ret == GST_STATE_CHANGE_FAILURE)
goto failed;
state = GST_STATE_PAUSED;
}
return;
failed:
{
g_print ("PAUSE failed\n");
}
}
static void
pause_cb (GtkButton * button, gpointer data)
{
if (app_playback_stream_id == -1)
g_warning("app have no playback stream\n");
am_pause = FALSE;
am_stream_notify_pause(app_playback_stream_id, am_pause);
do_pause();
app_paused = TRUE;
update_pause_button(!app_paused);
update_resume_button(app_paused);
}
gint32 am_register()
{
gint32 s_output;
dbus_g_proxy_call (proxy, "Register", &error, G_TYPE_INVALID, G_TYPE_INT, &s_output, G_TYPE_INVALID);
return s_output;
}
gint32 am_unregister()
{
gint32 s_output;
dbus_g_proxy_call (proxy, "Unregister", &error, G_TYPE_INVALID, G_TYPE_INT, &s_output, G_TYPE_INVALID);
return s_output;
}
gint32 am_stream_notify_pause(guint32 stream_id, gboolean am_triggered)
{
gint32 s_output;
dbus_g_proxy_call (proxy, "StreamNotifyPause", &error, G_TYPE_UINT, stream_id, G_TYPE_BOOLEAN, am_triggered, G_TYPE_INVALID, G_TYPE_INT, &s_output, G_TYPE_INVALID);
return s_output;
}
gint32 am_stream_notify_resume(guint32 stream_id, gboolean am_triggered)
{
gint32 s_output;
dbus_g_proxy_call (proxy, "StreamNotifyResume", &error, G_TYPE_UINT, stream_id, G_TYPE_BOOLEAN, am_triggered, G_TYPE_INVALID, G_TYPE_INT, &s_output, G_TYPE_INVALID);
return s_output;
}
/* AM Signal handlers
* */
void am_stream_destroyed_signal_handler(DBusGProxy* p, guint32 stream_id, gpointer userData) {
g_print("Got signal AMStreamDestroyed, stream id is %d\n", stream_id);
if (app_playback_stream_id == stream_id)
{
app_playback_stream_id = -1;
}
}
void am_stream_created_signal_handler(DBusGProxy* p, gchar* binary_name, gchar* stream_name, guint32 stream_id, guint32 type, gpointer userData) {
g_print("Got signal AMStreamCreated, binary_name is %s, stream_name is %s, stream_id is %d, type is %d\n", binary_name, stream_name, stream_id, type);
if (g_strcasecmp(app_binary_name, binary_name) == 0) /* neglect stream_name, type */
{
app_playback_stream_id = stream_id;
}
}
void am_stream_muted_signal_handler(DBusGProxy* p, guint32 stream_id, gboolean pause, gpointer userData) {
g_print("Got signal AMStreamMutedSignal, stream id is %d, pause is ", stream_id);
if (pause)
g_print("TRUE\n");
else
g_print("FALSE\n");
if (stream_id != app_playback_stream_id)
{
g_print("Not my signal, DO NOTHING\n");
return;
}
if (app_playback_stream_id == -1)
g_warning("app have no playback stream\n");
am_mute = TRUE;
if (pause)
{
am_pause = TRUE;
am_stream_notify_pause(app_playback_stream_id, am_pause);
do_pause();
app_paused = TRUE;
if (app_video)
app_can_resume = TRUE;
else
app_can_resume = FALSE;
update_resume_button(app_can_resume);
update_pause_button(!app_paused);
}
}
void am_stream_unmuted_signal_handler(DBusGProxy* p, guint32 stream_id, gboolean resume, gpointer userData) {
g_print("Got signal AMStreamUnMutedSignal, stream id is %d, resume is ", stream_id);
if (resume)
g_print("TRUE\n");
else
g_print("FALSE\n");
if (stream_id != app_playback_stream_id)
{
g_print("Not my signal, DO NOTHING\n");
return;
}
if (app_playback_stream_id == -1)
g_warning("app have no playback stream\n");
am_mute = FALSE;
if (resume)
{
app_can_resume = TRUE;
if (am_pause)
{
g_print("Resume internal pause\n");
am_stream_notify_resume(app_playback_stream_id, TRUE);
do_resume();
app_paused = FALSE;
update_resume_button(app_paused);
update_pause_button(!app_paused);
}
else
{
g_print("Can't resume an explicit pause\n");
}
am_pause = FALSE;
}
}
/* Signal connection initialization
* */
void connect_signals() {
dbus_g_object_register_marshaller(marshal_VOID__UINT_UINT, G_TYPE_NONE, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_INVALID);
dbus_g_object_register_marshaller(marshal_VOID__STRING_STRING_UINT_UINT, G_TYPE_NONE, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_INVALID);
dbus_g_object_register_marshaller(marshal_VOID__UINT_BOOLEAN, G_TYPE_NONE, G_TYPE_UINT, G_TYPE_BOOLEAN, G_TYPE_INVALID);
dbus_g_proxy_add_signal(proxy, "StreamDestroyed", G_TYPE_UINT, G_TYPE_INVALID);
dbus_g_proxy_connect_signal(proxy, "StreamDestroyed", G_CALLBACK(am_stream_destroyed_signal_handler), NULL, NULL);
dbus_g_proxy_add_signal(proxy, "StreamCreated", G_TYPE_STRING, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_INVALID);
dbus_g_proxy_connect_signal(proxy, "StreamCreated", G_CALLBACK(am_stream_created_signal_handler), NULL, NULL);
dbus_g_proxy_add_signal(proxy, "StreamMuted", G_TYPE_UINT, G_TYPE_BOOLEAN, G_TYPE_INVALID);
dbus_g_proxy_connect_signal(proxy, "StreamMuted", G_CALLBACK(am_stream_muted_signal_handler), NULL, NULL);
dbus_g_proxy_add_signal(proxy, "StreamUnmuted", G_TYPE_UINT, G_TYPE_BOOLEAN, G_TYPE_INVALID);
dbus_g_proxy_connect_signal(proxy, "StreamUnmuted", G_CALLBACK(am_stream_unmuted_signal_handler), NULL, NULL);
}
/////////////////////////////////AM related above//////////////////////////////////////////////////////////////////////| Attachment | Size |
|---|---|
| seek.txt | 59.65 KB |
| marshal.c.txt | 6.89 KB |
| marshal.h.txt | 1.52 KB |
| marshal.list.txt | 62 bytes |
- Printer-friendly version
- Login or register to post comments
Comments (6 total)
ALSA APP is limited
As we know, there is plenty of existed ALSA applications. If ALSA app can not be compliant orient, it would be a great regret. Is it possible to support ALSA app more elegantly.
ALSA app will be muted
There's no way to support ALSA app more elegantly, since ALSA plugin for PA is implemented in that way.
But they could be muted
How about rewriting ALSA plugin?
Can we rewrite ALSA plugin for PA to fix this issue?
I'm not sure
I'm not sure if ALSA plugins could be rewritten to fix the issue(actually it's not an issue if AM is not considered).
What is the difference of
What is the difference of "Fire-N-Forget" mode and "Decode-N-Return" mode? Does the former means audio data will be transfer to LPE directly and bypass PA, and the latter means use LPE to decode audio data and pass the pure PCM data to PA?
Yes, you are right
Yes, you are right;