Üdvözöljük az ubuntu.hu oldalán

Itt megtalálhatja a rendszerrel, illetve a nyílt forráskódú alkalmazásokkal kapcsolatos információkat, érdekességeket. Csatlakozzon a beszélgetésekhez, blogoljon, segítse Ön is a közösséget. Jó fórumozást kívánunk!
EgyébEgyéb GNU/Linux disztribúciókBlogokLeírások, bemutatók

Pulseaudio jelszint figyelése, avagy erősítő bekapcsolása amikor tényleg kell

Kiindulásként adott már egy Raspberry 3B+, ami számos funkciót lát el a nappalinkban fizikailag rejtve.
Ehhez legutóbb szerkintettem egy relémodult, amiből eddig még csak az egyik relét használtam a Settop box lekapcsolására.
https://ubuntu.hu/blog/46078-barkacs-25kwhev/29

Az egyik funkciója ennek a Raspberrynek, hogy a hálózaton kihelyezett hangkártyaként is viselkedik (Pulseadudio network sink, de emellett A2DP Bluetooth vevő is egyben), ami végül is most egy HiFiBerry-n keresztül szól.

Sajnos, az erősítőmnek a távirányítóján kipurcant a ki-/bekapcsológomb. Néha még sikerül úgy megnyomni, hogy működjön, de többnyire már nem. (Nem, nem az elem merült ki benne 🙂 )
Az utóbbi nagyjából 2 évben már kizárólag Spotify-t hallgatok rajta, szóval felmerült bennem, hogy milyen klassz is lenne, ha nem nekem kéne bekapcsolnom az erősítőt, hanem magától indulna, és kapcsolódna ki, amikor már nem kell.
Feltúrtam hát a virtuális LEGO-s dobozt, hogy milyen építőelemeket találok, amivel valahogy elérhetem, amit elképzeltem, vagy legalább valami hasonlót.
Amit találtam:
pactl subscribe
peak-detect.py
https://gitlab.com/menn0/peak-detect

aminek függősége:
https://github.com/Valodim/python-pulseaudio

(Ezt előbb telepíteni kell, hogy a peakdetect működjön: letöltés, majd setup.py install )

Ez, amit most összeraktam, úgy működik, hogyha hang érkezik a HiFiBerry-re, az erősítőt azonnal bekapcsolja, ha a hang eltűnik, nagyjából 5 perc múlva kikapcsolja (direkt nem azonnal). Amúgy pontosan ezt álmodtam meg eredetileg 😁

És ahogy összeállt:

Kicsit beleberheltem a peakdetect-be, lényegében alaposan lebutítottam, mert nem kell a „bar”, meg a sorkitöltő space, elég a minta értéke, és nem kell túl sűrűn sem másodpercenként 2-3 untig sok, viszont a 8 bites minta kevés volt, mert extrém halk részeknél bár a zene szól, csupa 0-t hoz (például amit M. R. követett el, a Boléro eleje pont ilyen), kikommenteltem az induláskori tájékoztató üzeneteit, mert csak útban voltak nekem, és csak a hibaüzeneteit hagytam meg, ha esetleg..... ; most ilyen:

/opt/peakdetect.py

#!/usr/bin/python
import sys
from Queue import Queue
from ctypes import POINTER, c_ubyte, c_void_p, c_ulong, cast

# From https://github.com/Valodim/python-pulseaudio
from pulseaudio.lib_pulseaudio import *

SINK_NAME = 'alsa_output.platform-soc_sound.stereo-fallback'  # edit to match your sink
METER_RATE = 2
#MAX_SAMPLE_VALUE = 127
#DISPLAY_SCALE = 1
#MAX_SPACES = MAX_SAMPLE_VALUE >> DISPLAY_SCALE

class PeakMonitor(object):

    def __init__(self, sink_name, rate):
        self.sink_name = sink_name
        self.rate = rate

        # Wrap callback methods in appropriate ctypefunc instances so
        # that the Pulseaudio C API can call them
        self._context_notify_cb = pa_context_notify_cb_t(self.context_notify_cb)
        self._sink_info_cb = pa_sink_info_cb_t(self.sink_info_cb)
        self._stream_read_cb = pa_stream_request_cb_t(self.stream_read_cb)

        # stream_read_cb() puts peak samples into this Queue instance
        self._samples = Queue()

        # Create the mainloop thread and set our context_notify_cb
        # method to be called when there's updates relating to the
        # connection to Pulseaudio
        _mainloop = pa_threaded_mainloop_new()
        _mainloop_api = pa_threaded_mainloop_get_api(_mainloop)
        context = pa_context_new(_mainloop_api, 'peak_demo')
        pa_context_set_state_callback(context, self._context_notify_cb, None)
        pa_context_connect(context, None, 0, None)
        pa_threaded_mainloop_start(_mainloop)

    def __iter__(self):
        while True:
            yield self._samples.get()

    def context_notify_cb(self, context, _):
        state = pa_context_get_state(context)

        if state == PA_CONTEXT_READY:
#            print "Pulseaudio connection ready..."
            # Connected to Pulseaudio. Now request that sink_info_cb
            # be called with information about the available sinks.
            o = pa_context_get_sink_info_list(context, self._sink_info_cb, None)
            pa_operation_unref(o)

        elif state == PA_CONTEXT_FAILED :
            print "Connection failed"

        elif state == PA_CONTEXT_TERMINATED:
            print "Connection terminated"

    def sink_info_cb(self, context, sink_info_p, _, __):
        if not sink_info_p:
            return

        sink_info = sink_info_p.contents
#        print '-'* 60
#       print 'index:', sink_info.index
#        print 'name:', sink_info.name
#        print 'description:', sink_info.description

        if sink_info.name == self.sink_name:
            # Found the sink we want to monitor for peak levels.
            # Tell PA to call stream_read_cb with peak samples.
#            print
#            print 'setting up peak recording using', sink_info.monitor_source_name
#            print
            samplespec = pa_sample_spec()
            samplespec.channels = 1
            samplespec.format = PA_SAMPLE_S16LE
            samplespec.rate = self.rate

            pa_stream = pa_stream_new(context, "peak detect demo", samplespec, None)
            pa_stream_set_read_callback(pa_stream,
                                        self._stream_read_cb,
                                        sink_info.index)
            pa_stream_connect_record(pa_stream,
                                     sink_info.monitor_source_name,
                                     None,
                                     PA_STREAM_PEAK_DETECT)

    def stream_read_cb(self, stream, length, index_incr):
        data = c_void_p()
        pa_stream_peek(stream, data, c_ulong(length))
        data = cast(data, POINTER(c_ulong))
        for i in xrange(length):
            # When PA_SAMPLE_U8 is used, samples values range from 128
            # to 255 because the underlying audio data is signed but
            # it doesn't make sense to return signed peaks.
            self._samples.put(data[i])
        pa_stream_drop(stream)

def main():
    monitor = PeakMonitor(SINK_NAME, METER_RATE)
    for sample in monitor:
        # sample = sample >> DISPLAY_SCALE
        #bar = '>' * sample
        #spaces = ' ' * (MAX_SPACES - sample)
        print '%s\n' %  (sample),
        sys.stdout.flush()

if __name__ == '__main__':
    main()

Ez így most csak számokat irkál ki folyamatosan, ha nem szól semmi, akkor csupa 0-t.

Összekalapáltam egy scriptet, ami feliratkozik a Pulseaudio eseményeire, és kifejezetten a 2. nyelőre figyel, mert az a HiFiBerry.
Ha az állapota „RUNNING”-ra vált, akkor elindul egy ciklusban a peakdetect, hogy figyelje, van-e hang.
Erre azért van szükség, mert amikor a hálózatomon a gépek elindulnak, amelyiken be van állítva, hogy küldhet hangot a Raspberry-re, a Pulseaudio egy rövid időre futó állapotba rakja a nyelőt. Emiatt nem akartam azonnal bekapcsolni az erősítőt, a „RUNNING” alapján, mert akkor fölöslegesen sokszor lenne bekapcsolva nagyon rövid időre. Amikor elindul a monitorpeak függvény, az először csak számolja a 0-kat, amiket a peakdetec.py irkál ki. Ha valami megszólal, a peakdetect azonnal 0-tól eltérő számokat kezd irkálni, erre az erősítő be lesz kapcsolva.
Ha a zene elhallgat, újból csak a 0-kat számolja. Ha több, mint 1100 összejött (ez nagyjából 5 perc), kilép a ciklusból, kinyírja a peakdetect.py-t és kikacsolja az erősítőt.
Ez az elmélet, tegnap óta működik így a gyakorlatban, és nagyon tetszik 🙂
De még figyelgetem, előjön-e valami bug?

És a script, ami /opt/amplifierswitch.sh néven fut:

#! /bin/sh

eroltetobe ()
{
echo "0" > /sys/class/gpio/gpio22/value
touch /tmp/ero-be
}

eroltetoki ()
{
rm /tmp/ero-be
echo "1" > /sys/class/gpio/gpio22/value
}

monitorpeak ()
{
zeros=0
/opt/peakdetect.py | while IFS= read -r peakevent; do
case $peakevent in
0)
zeros=$((zeros+1))
;;

"Connection failed")
pkill peakdetect.py
break
;;

"Connection terminated")
pkill peakdetect.py
break
;;

*)
zeros=0
if [ ! -f /tmp/ero-be ]; then
    echo "debug: erolteto be"
    eroltetobe
fi
;;

esac

if [ $zeros -gt 1100 ]; then
    pkill peakdetect.py
    break
fi
done;
echo "debug: erolteto ki"
eroltetoki
}

sinkstate=""
pactl subscribe | while IFS= read -r pactlevent; do
echo "debug: $pactlevent"
if [ "$pactlevent" = "„módosítás” esemény ezen: 2. nyelő" ];
 then
   prevstate=$sinkstate
   sinkstate=$(pactl list sinks short | grep platform-soc | awk '{print $7}')

   #sinkstate: RUNNING IDLE SUSPENDED
   echo "debug: $sinkstate"

   case $sinkstate in

	"RUNNING")
	if [ "$prevstate" = "RUNNING" ]; then
	   #nem valtozott, még jöhet ide valami
	   echo "debug: Running volt elobb is"

	else
	#most indult el a sink
	echo " debug: monitorpeak indul"
	monitorpeak&
	fi
	;;

	"IDLE")
	echo "debug: sink idle"
	;;

	"SUSPENDED")
	echo "debug: sink suspended"
	;;

   esac
fi
done

Ahhoz, hogy ez szépen a háttérben működjön, egy külön service-t csináltam belőle, itt kiemelném, hogy ugyanazon felhasználó nevében fut, mint a Pulseaudio démon:

/etc/systemd/system/amplifierswitcher.service tartalma:

[Unit]
Description=Amplifier Switcher
Requires=pulseaudio.service
After=pulseaudio

[Service]
Type=simple
User=pulse
ExecStart=/opt/amplifierswitch.sh

[Install]
WantedBy=multi-user.target

Szokásos: systemctl daemon-reload; systemctl enable amplifierswitcher

Annyi még, hogy ahhoz, hogy a kapcsolgatást tényleg meg tudja csinálni, a „pulse” felhasználót hozzá kell adni a „gpio” csoporthoz:
usermod -a -G gpio pulse

Most még tervezem, hogy felteszek egy lighttpd -t, és rityyentek rá valami egyszerű webes felületet, amin pulseaudio-tól függetlenül is be tudom kapcsolni, ha esetleg valami más műsorforrás miatt kéne....
Ahhoz majd shell scripteket fogok használni cgi-ként.