RSS Amplifier

menno.io · May 8, 2013

Monitoring Audio Levels with PulseAudio

0
Sign in to vote or save

This page cannot be shown here. You can still read it on the original site — the toolbar below keeps your place in the directory.

I'm working on driving an analog VU meter from my Raspberry Pi using whatever audio is going out the Pi's sound outputs. The de facto Linux sound system, PulseAudio , allows any sound output (or "sink" in PulseAudio's nonclementure) to be monitored. In PulseAudio land, each sink has a corresponding "source" called the monitor source which can be read just like any other other…

I'm working on driving an analog VU meter</a> from my Raspberry Pi</a> using whatever audio is going out the Pi's sound outputs. The de facto Linux sound system, PulseAudio</a>, allows any sound output (or "sink" in PulseAudio's nonclementure) to be monitored. In PulseAudio land, each sink has a corresponding "source" called the monitor source which can be read just like any other other PulseAudio input such as a microphone. In fact, to help with volume meter style applications, PulseAudio even allows you to ask for peak level measurements, which means you can sample the monitor sink at a low frequency, with low CPU utilisation, but still produce a useful volume display. When this feature is used, each sample read indicates the peak level since the last sample.</p>

The main PulseAudio API is asynchronous and callback based, and the documentation</a> is primarly just an API reference. This makes it a little difficult to figure out how to get everthing to hang together. Using the code from various open source projects (primarily veromix-plasmoid</a> and pavumeter</a>), along with the API reference, I was able to develop a fairly minimal code example that will hopefully be useful to others trying to do something similar. Although this example is written in Python, it is using the PulseAudio C API directly (via ctypes) so it should hopefully still be relevant if your application is written in C or another language.</p>

Here's the demo code. The latest version is also available on GitLab</a>. Note that in order to run this example you need to install Vincent Breitmoser's ctypes PulseAudio wrapper available at https://github.com/Valodim/python-pulseaudio</a>.</p>

import</span> sys</span></span>
from</span> Queue</span> import</span> Queue</span></span>
from</span> ctypes</span> import</span> POINTER</span>, c_ubyte, c_void_p, c_ulong, cast</span></span>
</span>
# From https://github.com/Valodim/python-pulseaudio</span></span>
from</span> pulseaudio.lib_pulseaudio</span> import *</span></span>
</span>
# edit to match your sink</span></span>
SINK_NAME</span> =</span> 'alsa_output.pci-0000_00_1b.0.analog-stereo'</span></span>
METER_RATE</span> =</span> 344</span></span>
MAX_SAMPLE_VALUE</span> =</span> 127</span></span>
DISPLAY_SCALE</span> =</span> 2</span></span>
MAX_SPACES</span> =</span> MAX_SAMPLE_VALUE</span> >></span> DISPLAY_SCALE</span></span>
</span>
class</span> PeakMonitor</span>(</span>object</span>):</span></span>
</span>
    def</span> __init__</span>(self, sink_name, rate):</span></span>
        self</span>.sink_name</span> =</span> sink_name</span></span>
        self</span>.rate</span> =</span> rate</span></span>
</span>
        # Wrap callback methods in appropriate ctypefunc instances so</span></span>
        # that the Pulseaudio C API can call them</span></span>
        self</span>._context_notify_cb</span> =</span> pa_context_notify_cb_t(</span>self</span>.context_notify_cb)</span></span>
        self</span>._sink_info_cb</span> =</span> pa_sink_info_cb_t(</span>self</span>.sink_info_cb)</span></span>
        self</span>._stream_read_cb</span> =</span> pa_stream_request_cb_t(</span>self</span>.stream_read_cb)</span></span>
</span>
        # stream_read_cb() puts peak samples into this Queue instance</span></span>
        self</span>._samples</span> =</span> Queue()</span></span>
</span>
        # Create the mainloop thread and set our context_notify_cb</span></span>
        # method to be called when there's updates relating to the</span></span>
        # connection to Pulseaudio</span></span>
        _mainloop</span> =</span> pa_threaded_mainloop_new()</span></span>
        _mainloop_api</span> =</span> pa_threaded_mainloop_get_api(_mainloop)</span></span>
        context</span> =</span> pa_context_new(_mainloop_api,</span> 'peak_demo'</span>)</span></span>
        pa_context_set_state_callback(context,</span> self</span>._context_notify_cb,</span> None</span>)</span></span>
        pa_context_connect(context,</span> None</span>,</span> 0</span>,</span> None</span>)</span></span>
        pa_threaded_mainloop_start(_mainloop)</span></span>
</span>
    def</span> __iter__</span>(self):</span></span>
        while</span> True</span>:</span></span>
            yield</span> self</span>._samples.get()</span></span>
</span>
    def</span> context_notify_cb</span>(self, context, _):</span></span>
        state</span> =</span> pa_context_get_state(context)</span></span>
</span>
        if</span> state</span> ==</span> PA_CONTEXT_READY</span>:</span></span>
            print</span> "Pulseaudio connection ready..."</span></span>
            # Connected to Pulseaudio. Now request that sink_info_cb</span></span>
            # be called with information about the available sinks.</span></span>
            o</span> =</span> pa_context_get_sink_info_list(context,</span> self</span>._sink_info_cb,</span> None</span>)</span></span>
            pa_operation_unref(o)</span></span>
</span>
        elif</span> state</span> ==</span> PA_CONTEXT_FAILED</span> :</span></span>
            print</span> "Connection failed"</span></span>
</span>
        elif</span> state</span> ==</span> PA_CONTEXT_TERMINATED</span>:</span></span>
            print</span> "Connection terminated"</span></span>
</span>
    def</span> sink_info_cb</span>(self, context, sink_info_p, _, __):</span></span>
        if not</span> sink_info_p:</span></span>
            return</span></span>
</span>
        sink_info</span> =</span> sink_info_p.contents</span></span>
        print</span> '-'</span>*</span> 60</span></span>
        print</span> 'index:'</span>, sink_info.index</span></span>
        print</span> 'name:'</span>, sink_info.name</span></span>
        print</span> 'description:'</span>, sink_info.description</span></span>
</span>
        if</span> sink_info.name</span> ==</span> self</span>.sink_name:</span></span>
            # Found the sink we want to monitor for peak levels.</span></span>
            # Tell PA to call stream_read_cb with peak samples.</span></span>
            print</span></span>
            print</span> 'setting up peak recording using'</span>, sink_info.monitor_source_name</span></span>
            print</span></span>
            samplespec</span> =</span> pa_sample_spec()</span></span>
            samplespec.channels</span> =</span> 1</span></span>
            samplespec.format</span> =</span> PA_SAMPLE_U8</span></span>
            samplespec.rate</span> =</span> self</span>.rate</span></span>
</span>
            pa_stream</span> =</span> pa_stream_new(context,</span> "peak detect demo"</span>, samplespec,</span> None</span>)</span></span>
            pa_stream_set_read_callback(pa_stream,</span></span>
                                        self</span>._stream_read_cb,</span></span>
                                        sink_info.index)</span></span>
            pa_stream_connect_record(pa_stream,</span></span>
                                     sink_info.monitor_source_name,</span></span>
                                     None</span>,</span></span>
                                     PA_STREAM_PEAK_DETECT</span>)</span></span>
</span>
    def</span> stream_read_cb</span>(self, stream, length, index_incr):</span></span>
        data</span> =</span> c_void_p()</span></span>
        pa_stream_peek(stream, data, c_ulong(length))</span></span>
        data</span> =</span> cast(data, POINTER(c_ubyte))</span></span>
        for</span> i</span> in</span> xrange</span>(length):</span></span>
            # When PA_SAMPLE_U8 is used, samples values range from 128</span></span>
            # to 255 because the underlying audio data is signed but</span></span>
            # it doesn't make sense to return signed peaks.</span></span>
            self</span>._samples.put(data[i]</span> -</span> 128</span>)</span></span>
        pa_stream_drop(stream)</span></span>
</span>
def</span> main</span>():</span></span>
    monitor</span> =</span> PeakMonitor(</span>SINK_NAME</span>,</span> METER_RATE</span>)</span></span>
    for</span> sample</span> in</span> monitor:</span></span>
        sample</span> =</span> sample</span> >></span> DISPLAY_SCALE</span></span>
        bar</span> =</span> '>'</span> *</span> sample</span></span>
        spaces</span> =</span> ' '</span> *</span> (</span>MAX_SPACES</span> -</span> sample)</span></span>
        print</span> '</span> %3d %s%s\r</span>'</span> %</span> (sample, bar, spaces),</span></span>
        sys.stdout.flush()</span></span>
</span>
if</span> __name__</span> ==</span> '__main__'</span>:</span></span>
    main()</span></span></code></pre>

When running this demo, you'll need to modify SINK_NAME to match the name of the sink you want to monitor. If you're not sure of the sink, just run it - the program prints the details of all available sinks to stdout. If all goes to plan you should see a basic volume display in the console (when sound is actually playing!).</p>

In this demo, the PeakMonitor class does all the interaction with the PulseAudio API. It needs the name of a sink to monitor and the sampling rate. Iterating over a PeakMonitor instance will give 8-bit peak level samples (actually they're from 128 to 255 - see comment in code and comment from Tanu on this article).</p>

The main</code> function implements a simple volume display. Some of the constants at the top of the code control how the volume level is displayed.</p>

As mentioned earlier, the PulseAudio API is asynchronous, making heavy use of callbacks. This goes for API calls that return information about the sound configuration of the system as well as API calls that return or accept actual sound data. The reason for this is that the available sound devices may change at any point as a program is running (e.g. when USB audio devices are connected) and PulseAudio clients may need to be able to handle such changes.</p>

Here's how the demo program interacts with PulseAudio:</p>

Read on /posts/pulseaudio_monitoring/

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.