Skip to content
Snippets Groups Projects
PulseAudioSystem.java 18.12 KiB
/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jitsi.impl.neomedia.device;

import java.io.*;
import java.util.*;

import javax.media.*;
import javax.media.format.*;

import org.jitsi.impl.neomedia.*;
import org.jitsi.impl.neomedia.jmfext.media.renderer.audio.*;
import org.jitsi.impl.neomedia.pulseaudio.*;
import org.jitsi.service.version.*;

public class PulseAudioSystem
    extends AudioSystem
{
    private static final String LOCATOR_PROTOCOL = LOCATOR_PROTOCOL_PULSEAUDIO;

    public static final String MEDIA_ROLE_EVENT = "event";

    public static final String MEDIA_ROLE_PHONE = "phone";

    private static final String NULL_DEV_CAPTURE_DEVICE_INFO_NAME = "Default";

    public static void corkStream(long stream, boolean b)
        throws IOException
    {
        if (stream == 0)
            throw new IOException("stream");

        long o = PA.stream_cork(stream, b, null);

        if (o == 0)
            throw new IOException("pa_stream_cork");

        PA.operation_unref(o);
    }

    public static PulseAudioSystem getPulseAudioSystem()
    {
        AudioSystem audioSystem
            = AudioSystem.getAudioSystem(PulseAudioSystem.LOCATOR_PROTOCOL);

        return
            (audioSystem instanceof PulseAudioSystem)
                ? (PulseAudioSystem) audioSystem
                : null;
    }

    private boolean createContext;

    private long context;

    private long mainloop;

    public PulseAudioSystem()
        throws Exception
    {
        super(LOCATOR_PROTOCOL, FEATURE_NOTIFY_AND_PLAYBACK_DEVICES);
    }

    private void createContext()
    {
        if (this.context != 0)
            throw new IllegalStateException("context");

        startMainloop();
        try
        {
            long proplist = PA.proplist_new();

            if (proplist == 0)
                throw new RuntimeException("pa_proplist_new");

            try
            {
                populateContextProplist(proplist);

                long context
                    = PA.context_new_with_proplist(
                            PA.threaded_mainloop_get_api(mainloop),
                            null /* PA_PROP_APPLICATION_NAME */,
                            proplist);

                if (context == 0)
                    throw new RuntimeException("pa_context_new_with_proplist");

                try
                {
                    if (proplist != 0)
                    {
                        PA.proplist_free(proplist);
                        proplist = 0;
                    }

                    Runnable stateCallback
                        = new Runnable()
                        {
                            public void run()
                            {
                                signalMainloop(false);
                            }
                        };

                    lockMainloop();
                    try
                    {
                        PA.context_set_state_callback(context, stateCallback);
                        PA.context_connect(
                                context,
                                null,
                                PA.CONTEXT_NOFLAGS,
                                0);

                        try
                        {
                            int state
                                = waitForContextState(
                                        context,
                                        PA.CONTEXT_READY);

                            if (state != PA.CONTEXT_READY)
                                throw new IllegalStateException(
                                        "context.state");

                            this.context = context;
                        }
                        finally
                        {
                            if (this.context == 0)
                                PA.context_disconnect(context);
                        }
                    }
                    finally
                    {
                        unlockMainloop();
                    }
                }
                finally
                {
                    if (this.context == 0)
                        PA.context_unref(context);
                }
            }
            finally
            {
                if (proplist != 0)
                    PA.proplist_free(proplist);
            }
        }
        finally
        {
            if (this.context == 0)
                stopMainloop();
        }
    }

    @Override
    public Renderer createRenderer(boolean playback)
    {
        MediaLocator locator;

        if (playback)
            locator = null;
        else
        {
            CaptureDeviceInfo notifyDevice
                = getDevice(AudioSystem.NOTIFY_INDEX);

            if (notifyDevice == null)
                return null;
            else
                locator = notifyDevice.getLocator();
        }

        PulseAudioRenderer renderer
            = new PulseAudioRenderer(
                    playback ? MEDIA_ROLE_PHONE : MEDIA_ROLE_EVENT);

        if ((renderer != null) && (locator != null))
            renderer.setLocator(locator);

        return renderer;
    }

    public long createStream(
            int sampleRate,
            int channels,
            String mediaName,
            String mediaRole)
        throws IllegalStateException,
               RuntimeException
    {
        long context = getContext();

        if (context == 0)
            throw new IllegalStateException("context");

        long sampleSpec
            = PA.sample_spec_new(PA.SAMPLE_S16LE, sampleRate, channels);

        if (sampleSpec == 0)
            throw new RuntimeException("pa_sample_spec_new");
        long ret = 0;

        try
        {
            long proplist = PA.proplist_new();

            if (proplist == 0)
                throw new RuntimeException("pa_proplist_new");

            try
            {
                PA.proplist_sets(proplist, PA.PROP_MEDIA_NAME, mediaRole);
                PA.proplist_sets(proplist, PA.PROP_MEDIA_ROLE, mediaRole);

                long stream
                    = PA.stream_new_with_proplist(
                            context,
                            null,
                            sampleSpec,
                            0,
                            proplist);

                if (stream == 0)
                    throw new RuntimeException(
                            "pa_stream_new_with_proplist");

                try
                {
                    ret = stream;
                }
                finally
                {
                    if (ret == 0)
                        PA.stream_unref(stream);
                }
            }
            finally
            {
                if (proplist != 0)
                    PA.proplist_free(proplist);
            }
        }
        finally
        {
            if (sampleSpec != 0)
                PA.sample_spec_free(sampleSpec);
        }

        return ret;
    }

    protected synchronized void doInitialize()
        throws Exception
    {
        long context = getContext();

        final List<ExtendedCaptureDeviceInfo> captureDevices
            = new LinkedList<ExtendedCaptureDeviceInfo>();
        final List<Format> captureDeviceFormats = new LinkedList<Format>();
        PA.source_info_cb_t sourceInfoListCallback
            = new PA.source_info_cb_t()
            {
                public void callback(long c, long i, int eol)
                {
                    try
                    {
                        if ((eol == 0) && (i != 0))
                        {
                            sourceInfoListCallback(
                                    c,
                                    i,
                                    captureDevices,
                                    captureDeviceFormats);
                        }
                    }
                    finally
                    {
                        signalMainloop(false);
                    }
                }
            };

        final List<ExtendedCaptureDeviceInfo> playbackDevices
            = new LinkedList<ExtendedCaptureDeviceInfo>();
        final List<Format> playbackDeviceFormats = new LinkedList<Format>();
        PA.sink_info_cb_t sinkInfoListCallback
            = new PA.sink_info_cb_t()
            {
                public void callback(long c, long i, int eol)
                {
                    try
                    {
                        if ((eol == 0) && (i != 0))
                        {
                            sinkInfoListCallback(
                                    c,
                                    i,
                                    playbackDevices,
                                    playbackDeviceFormats);
                        }
                    }
                    finally
                    {
                        signalMainloop(false);
                    }
                }
            };

        lockMainloop();
        try
        {
            long o
                = PA.context_get_source_info_list(
                        context,
                        sourceInfoListCallback);

            if (o == 0)
                throw new RuntimeException("pa_context_get_source_info_list");

            try
            {
                while (PA.operation_get_state(o) == PA.OPERATION_RUNNING)
                    waitMainloop();
            }
            finally
            {
                PA.operation_unref(o);
            }

            o
                = PA.context_get_sink_info_list(
                        context,
                        sinkInfoListCallback);

            if (o == 0)
                throw new RuntimeException("pa_context_get_sink_info_list");

            try
            {
                while (PA.operation_get_state(o) == PA.OPERATION_RUNNING)
                    waitMainloop();
            }
            finally
            {
                PA.operation_unref(o);
            }
        }
        finally
        {
            unlockMainloop();
        }

        if (!captureDeviceFormats.isEmpty())
        {
            captureDevices.add(
                    0,
                    new ExtendedCaptureDeviceInfo(
                            NULL_DEV_CAPTURE_DEVICE_INFO_NAME,
                            new MediaLocator(LOCATOR_PROTOCOL + ":"),
                            captureDeviceFormats.toArray(
                                    new Format[captureDeviceFormats.size()]),
                            null,
                            null));
        }
        if (!playbackDevices.isEmpty())
        {
            playbackDevices.add(
                    0,
                    new ExtendedCaptureDeviceInfo(
                            NULL_DEV_CAPTURE_DEVICE_INFO_NAME,
                            new MediaLocator(LOCATOR_PROTOCOL + ":"),
                            null,
                            null,
                            null));
        }

        setCaptureDevices(captureDevices);
        setPlaybackDevices(playbackDevices);
    }

    public synchronized long getContext()
    {
        if (context == 0)
        {
            if (!createContext)
            {
                createContext = true;
                createContext();
            }
            if (context == 0)
                throw new IllegalStateException("context");
        }
        return context;
    }

    public void lockMainloop()
    {
        PA.threaded_mainloop_lock(mainloop);
    }

    /**
     * Populates a specific <tt>pa_proplist</tt> which is to be used with a
     * <tt>pa_context</tt> with properties such as the application name and
     * version.
     *
     * @param proplist the <tt>pa_proplist</tt> which is to be populated with
     * <tt>pa_context</tt>-related properties such as the application name and
     * version
     */
    private void populateContextProplist(long proplist)
    {
        /*
         * XXX For the sake of simplicity while working on libjitsi, get the
         * version information in the form of System property values instead of
         * going through the VersionService.
         */
        String applicationName
            = System.getProperty(Version.PNAME_APPLICATION_NAME);

        if (applicationName != null)
            PA.proplist_sets(
                    proplist,
                    PA.PROP_APPLICATION_NAME,
                    applicationName);

        String applicationVersion
            = System.getProperty(Version.PNAME_APPLICATION_VERSION);

        if (applicationVersion != null)
            PA.proplist_sets(
                    proplist,
                    PA.PROP_APPLICATION_VERSION,
                    applicationVersion);
    }

    public void signalMainloop(boolean waitForAccept)
    {
        PA.threaded_mainloop_signal(mainloop, waitForAccept);
    }

    private void sinkInfoListCallback(
            long context,
            long sinkInfo,
            List<ExtendedCaptureDeviceInfo> deviceList,
            List<Format> formatList)
    {
        int sampleSpecFormat = PA.sink_info_get_sample_spec_format(sinkInfo);

        if (sampleSpecFormat != PA.SAMPLE_S16LE)
            return;

        String description = PA.sink_info_get_description(sinkInfo);
        String name = PA.sink_info_get_name(sinkInfo);

        if (description == null)
            description = name;
        deviceList.add(
                new ExtendedCaptureDeviceInfo(
                        description,
                        new MediaLocator(
                                LOCATOR_PROTOCOL
                                    + ":"
                                    + name),
                        null,
                        null,
                        null));
    }

    private void sourceInfoListCallback(
            long context,
            long sourceInfo,
            List<ExtendedCaptureDeviceInfo> deviceList,
            List<Format> formatList)
    {
        int monitorOfSink = PA.source_info_get_monitor_of_sink(sourceInfo);

        if (monitorOfSink != PA.INVALID_INDEX)
            return;

        int sampleSpecFormat
            = PA.source_info_get_sample_spec_format(sourceInfo);

        if (sampleSpecFormat != PA.SAMPLE_S16LE)
            return;

        int channels = PA.source_info_get_sample_spec_channels(sourceInfo);
        int rate = PA.source_info_get_sample_spec_rate(sourceInfo);
        List<Format> sourceInfoFormatList = new LinkedList<Format>();

        if ((MediaUtils.MAX_AUDIO_CHANNELS != Format.NOT_SPECIFIED)
                && (MediaUtils.MAX_AUDIO_CHANNELS < channels))
            channels = MediaUtils.MAX_AUDIO_CHANNELS;
        if ((MediaUtils.MAX_AUDIO_SAMPLE_RATE != Format.NOT_SPECIFIED)
                && (MediaUtils.MAX_AUDIO_SAMPLE_RATE < rate))
            rate = (int) MediaUtils.MAX_AUDIO_SAMPLE_RATE;

        AudioFormat audioFormat
            = new AudioFormat(
                AudioFormat.LINEAR,
                rate,
                16,
                channels,
                AudioFormat.LITTLE_ENDIAN,
                AudioFormat.SIGNED,
                Format.NOT_SPECIFIED /* frameSizeInBits */,
                Format.NOT_SPECIFIED /* frameRate */,
                Format.byteArray);

        if (!sourceInfoFormatList.contains(audioFormat))
        {
            sourceInfoFormatList.add(audioFormat);
            if (!formatList.contains(audioFormat))
                formatList.add(audioFormat);
        }
        if (!formatList.isEmpty())
        {
            String description = PA.source_info_get_description(sourceInfo);
            String name = PA.source_info_get_name(sourceInfo);

            if (description == null)
                description = name;
            deviceList.add(
                    new ExtendedCaptureDeviceInfo(
                            description,
                            new MediaLocator(
                                    LOCATOR_PROTOCOL
                                        + ":"
                                        + name),
                            sourceInfoFormatList.toArray(
                                    new Format[sourceInfoFormatList.size()]),
                            null,
                            null));
        }
    }

    private void startMainloop()
    {
        if (this.mainloop != 0)
            throw new IllegalStateException("mainloop");

        long mainloop = PA.threaded_mainloop_new();

        if (mainloop == 0)
            throw new RuntimeException("pa_threaded_mainloop_new");

        try
        {
            if (PA.threaded_mainloop_start(mainloop) < 0)
                throw new RuntimeException("pa_threaded_mainloop_start");
            this.mainloop = mainloop;
        }
        finally
        {
            if (this.mainloop == 0)
                PA.threaded_mainloop_free(mainloop);
        }
    }

    private void stopMainloop()
    {
        if (this.mainloop == 0)
            throw new IllegalStateException("mainloop");

        long mainloop = this.mainloop;

        this.mainloop = 0;
        PA.threaded_mainloop_stop(mainloop);
        PA.threaded_mainloop_free(mainloop);
    }

    @Override
    public String toString()
    {
        return "PulseAudio";
    }

    public void unlockMainloop()
    {
        PA.threaded_mainloop_unlock(mainloop);
    }

    private int waitForContextState(
            long context,
            int stateToWaitFor)
    {
        int state;

        while (true)
        {
            state = PA.context_get_state(context);
            if ((PA.CONTEXT_FAILED == state)
                    || (stateToWaitFor == state)
                    || (PA.CONTEXT_TERMINATED == state))
                break;

            waitMainloop();
        }

        return state;
    }

    public int waitForStreamState(
            long stream,
            int stateToWaitFor)
    {
        int state;

        while (true)
        {
            state = PA.stream_get_state(stream);
            if ((stateToWaitFor == state)
                    || (PA.STREAM_FAILED == state)
                    || (PA.STREAM_TERMINATED == state))
                break;

            waitMainloop();
        }

        return state;
    }

    public void waitMainloop()
    {
        PA.threaded_mainloop_wait(mainloop);
    }
}